diff --git a/backend/core/src/commands.cpp b/backend/core/src/commands.cpp
index 467c43c8..b6f7b34e 100644
--- a/backend/core/src/commands.cpp
+++ b/backend/core/src/commands.cpp
@@ -264,7 +264,7 @@ string Laser::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
- commandSS << "Olympus.laser, "
+ commandSS << "Olympus.fireLaser, "
<< ID << ", "
<< code << ", "
<< destination.lat << ", "
@@ -277,7 +277,7 @@ string Infrared::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
- commandSS << "Olympus.infrared, "
+ commandSS << "Olympus.fireInfrared, "
<< ID << ", "
<< destination.lat << ", "
<< destination.lng;
diff --git a/backend/core/src/server.cpp b/backend/core/src/server.cpp
index e582d7b9..6fc893ca 100644
--- a/backend/core/src/server.cpp
+++ b/backend/core/src/server.cpp
@@ -128,6 +128,9 @@ void Server::handle_get(http_request request)
/* Bullseyes data */
else if (URI.compare(BULLSEYE_URI) == 0 && missionData.has_object_field(L"bullseyes"))
answer[L"bullseyes"] = missionData[L"bullseyes"];
+ /* Spots (laser/IR) data */
+ else if (URI.compare(SPOTS_URI) == 0 && missionData.has_object_field(L"spots"))
+ answer[L"spots"] = missionData[L"spots"];
/* Mission data */
else if (URI.compare(MISSION_URI) == 0 && missionData.has_object_field(L"mission")) {
answer[L"mission"] = missionData[L"mission"];
diff --git a/backend/shared/include/defines.h b/backend/shared/include/defines.h
index 4c121fe9..156d6c74 100644
--- a/backend/shared/include/defines.h
+++ b/backend/shared/include/defines.h
@@ -10,6 +10,7 @@
#define LOGS_URI "logs"
#define AIRBASES_URI "airbases"
#define BULLSEYE_URI "bullseyes"
+#define SPOTS_URI "spots"
#define MISSION_URI "mission"
#define COMMANDS_URI "commands"
diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts
index f9f3b4aa..79e5b304 100644
--- a/frontend/react/src/constants/constants.ts
+++ b/frontend/react/src/constants/constants.ts
@@ -39,6 +39,7 @@ export const WEAPONS_URI = "weapons";
export const LOGS_URI = "logs";
export const AIRBASES_URI = "airbases";
export const BULLSEYE_URI = "bullseyes";
+export const SPOTS_URI = "spots";
export const MISSION_URI = "mission";
export const COMMANDS_URI = "commands";
diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts
index 0515da63..6fe65d4c 100644
--- a/frontend/react/src/interfaces.ts
+++ b/frontend/react/src/interfaces.ts
@@ -80,6 +80,14 @@ export interface BullseyesData {
time: number;
}
+export interface SpotsData {
+ spots: {
+ [key: string]: { type: string, targetPosition: {lat: number; lng: number}; sourceUnitID: number; code?: number };
+ };
+ sessionHash: string;
+ time: number;
+}
+
export interface MissionData {
mission: {
theatre: string;
diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts
index 531d6c46..97f781a0 100644
--- a/frontend/react/src/map/map.ts
+++ b/frontend/react/src/map/map.ts
@@ -35,6 +35,7 @@ import { ContextAction } from "../unit/contextaction";
import "./markers/stylesheets/airbase.css";
import "./markers/stylesheets/bullseye.css";
import "./markers/stylesheets/units.css";
+import "./markers/stylesheets/spot.css";
import "./stylesheets/map.css";
import { initDraggablePath } from "./coalitionarea/draggablepath";
diff --git a/frontend/react/src/map/markers/spoteditmarker.ts b/frontend/react/src/map/markers/spoteditmarker.ts
new file mode 100644
index 00000000..c37aec0b
--- /dev/null
+++ b/frontend/react/src/map/markers/spoteditmarker.ts
@@ -0,0 +1,216 @@
+import { Marker, LatLng, DivIcon, DomEvent, Map } from "leaflet";
+
+export class SpotEditMarker extends Marker {
+ #textValue: string;
+ #isEditable: boolean = false;
+ #rotationAngle: number; // Rotation angle in radians
+ #previousValue: string;
+
+ constructor(latlng: LatLng, textValue: string, rotationAngle: number = 0) {
+ super(latlng, {
+ icon: new DivIcon({
+ className: "leaflet-spot-input-marker",
+ html: `
`,
+ }),
+ });
+
+ this.#textValue = textValue;
+ this.#rotationAngle = rotationAngle;
+ this.#previousValue = textValue;
+ }
+
+ onAdd(map: Map): this {
+ super.onAdd(map);
+ const element = this.getElement();
+ if (element) {
+ const text = element.querySelector(".text");
+ const button = element.querySelector(".delete");
+ 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));
+
+ // Add click event listener to delete spot
+ button?.addEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
+ button?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
+ button?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
+
+ // Add click event listener to input spot
+ 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("input", (ev) => this.#validateInput(ev));
+ }
+
+ return this;
+ }
+
+ 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");
+
+ // 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));
+
+ // Add click event listener to delete spot
+ button?.removeEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
+ button?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
+ button?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
+
+ // Add 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("input", (ev) => this.#validateInput(ev));
+ }
+
+ return this;
+ }
+
+ // Method to set the text value
+ setTextValue(textValue: string) {
+ this.#textValue = textValue;
+ const element = this.getElement();
+ if (element) {
+ const text = element.querySelector(".text");
+ if (text) text.textContent = textValue;
+ }
+ }
+
+ // Method to get the text value
+ getTextValue() {
+ return this.#textValue;
+ }
+
+ // Method to set the rotation angle in radians
+ setRotationAngle(angle: number) {
+ this.#rotationAngle = angle;
+ if (!this.#isEditable) {
+ this.#updateRotation();
+ }
+ }
+
+ // Method to get the rotation angle in radians
+ getRotationAngle() {
+ return this.#rotationAngle;
+ }
+
+ // Method to update the rotation angle to ensure the text is always readable
+ #updateRotation() {
+ const element = this.getElement();
+ if (element) {
+ const container = element.querySelector(".container") as HTMLDivElement;
+ if (container) {
+ let angle = this.#rotationAngle % (2 * Math.PI);
+ if (angle < 0) angle += 2 * Math.PI;
+ if (angle > Math.PI / 2 && angle < (3 * Math.PI) / 2) {
+ angle = (angle + Math.PI) % (2 * Math.PI); // Flip the text to be upright
+ }
+ const angleInDegrees = angle * (180 / Math.PI);
+ container.style.transform = `rotate(${angleInDegrees}deg)`;
+ }
+ }
+ }
+
+ #toggleEditMode(ev) {
+ this.#isEditable = !this.#isEditable;
+
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ const element = this.getElement();
+ if (element) {
+ const textElement = element.querySelector(".text");
+ const inputElement = element.querySelector(".input") as HTMLInputElement;
+ const buttonElement = element.querySelector(".delete");
+ const container = element.querySelector(".container") as HTMLDivElement;
+ if (this.#isEditable) {
+ // Rotate to horizontal and make text editable
+ if (textElement && inputElement && buttonElement) {
+ textElement.classList.add("edit-mode");
+ inputElement.style.display = "block";
+ inputElement.value = this.#textValue;
+ inputElement.focus();
+ buttonElement.textContent = "✔"; // Change to check mark
+ buttonElement.classList.add("edit-mode");
+ }
+ container.style.transform = `rotate(0deg)`;
+ } else {
+ // Save the edited text and revert to normal mode
+ if (textElement && inputElement && buttonElement) {
+ const newText = inputElement.value || this.#textValue;
+ this.#textValue = newText;
+ textElement.classList.remove("edit-mode");
+ inputElement.style.display = "none";
+ buttonElement.textContent = "X"; // Change to delete mark
+ buttonElement.classList.remove("edit-mode");
+ }
+ this.#updateRotation();
+ }
+ }
+ }
+
+ #stopEventPropagation(ev) {
+ ev.stopPropagation();
+ }
+
+ #validateInput(ev) {
+ const element = this.getElement();
+ if (element) {
+ const input = ev.target as HTMLInputElement;
+ const text = element.querySelector(".text");
+
+ // Validate the input value
+ const value = input.value;
+
+ // Check if the value is a partial valid input
+ 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
+ const isValid = /^[1][1-7][1-8][1-8]$/.test(value) && Number(value) <= 1788;
+
+ if (isPartialValid || isValid) {
+ if (text) text.textContent = value;
+ this.#previousValue = value;
+ } else {
+ this.#errorFunction();
+ input.value = this.#previousValue;
+ if (text) text.textContent = this.#previousValue;
+ }
+ }
+ }
+
+ #errorFunction() {
+ const element = this.getElement();
+ if (element) {
+ const input = element.querySelector(".input") as HTMLInputElement;
+ if (input) {
+ input.classList.add("error-flash");
+ setTimeout(() => {
+ input.classList.remove("error-flash");
+ }, 300); // Duration of the flash effect
+ }
+ }
+ }
+
+ #acceptInput(ev) {
+ if (ev.key === "Enter") this.#toggleEditMode(ev);
+ }
+}
diff --git a/frontend/react/src/map/markers/spotmarker.ts b/frontend/react/src/map/markers/spotmarker.ts
new file mode 100644
index 00000000..23f63d08
--- /dev/null
+++ b/frontend/react/src/map/markers/spotmarker.ts
@@ -0,0 +1,23 @@
+import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
+import { CustomMarker } from "./custommarker";
+
+export class SpotMarker extends CustomMarker {
+ constructor(latlng: LatLngExpression, options?: MarkerOptions) {
+ super(latlng, options);
+ this.options.interactive = false;
+ this.setZIndexOffset(9999);
+ }
+
+ createIcon() {
+ this.setIcon(
+ new DivIcon({
+ iconSize: [52, 52],
+ iconAnchor: [26, 26],
+ className: "leaflet-spot-marker",
+ })
+ );
+ var el = document.createElement("div");
+ el.classList.add("ol-spot-icon");
+ this.getElement()?.appendChild(el);
+ }
+}
diff --git a/frontend/react/src/map/markers/stylesheets/spot.css b/frontend/react/src/map/markers/stylesheets/spot.css
new file mode 100644
index 00000000..2eaf8182
--- /dev/null
+++ b/frontend/react/src/map/markers/stylesheets/spot.css
@@ -0,0 +1,90 @@
+.leaflet-spot-input-marker {
+ text-align: center;
+ display: flex !important;
+ justify-content: center;
+ align-items: center;
+}
+
+.leaflet-spot-input-marker .delete {
+ background-color: darkred;
+ color: #fffd;
+ border-radius: 999px;
+ font-size: 13px;
+ cursor: pointer;
+ height: 30px;
+ width: 30px;
+ font-weight: bold;
+ align-content: center;
+}
+
+.leaflet-spot-input-marker .delete:hover {
+ background-color: lightcoral;
+}
+
+.leaflet-spot-input-marker .container {
+ width: fit-content;
+ display: flex;
+ transform-origin: center;
+ background-color: var(--background-steel);
+ color: white;
+ border-radius: 999px;
+ font-size: 13px;
+ column-gap: 6px;
+ align-content: center;
+}
+
+.leaflet-spot-input-marker .text {
+ margin-left: 12px;
+ margin-top: auto;
+ margin-bottom: auto;
+}
+
+.leaflet-spot-input-marker .input {
+ display: none;
+ color: white;
+ appearance: none; /* Remove default appearance */
+ background-color: #333; /* Dark background */
+ border-radius: 8px; /* Rounded borders */
+ border: 1px solid #555; /* Border color */
+ padding: 4px 8px; /* Padding for better appearance */
+ outline: none; /* Remove default outline */
+ font-size: 13px;
+ width: 80px;
+}
+
+.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 */
+}
+
+.leaflet-spot-input-marker .container .input {
+ display: none;
+}
+
+.leaflet-spot-input-marker .container .text.edit-mode {
+ display: none;
+}
+
+.leaflet-spot-input-marker .input.error-flash {
+ animation: error-flash 0.3s;
+}
+
+@keyframes error-flash {
+ 0% {
+ border-color: #555;
+ }
+ 50% {
+ border-color: red;
+ }
+ 100% {
+ border-color: #555;
+ }
+}
+
+.leaflet-spot-input-marker .delete.edit-mode {
+ background-color: green;
+}
+
+.leaflet-spot-input-marker .delete.edit-mode:hover {
+ background-color: lightgreen;
+}
diff --git a/frontend/react/src/map/stylesheets/map.css b/frontend/react/src/map/stylesheets/map.css
index 6581d324..4cfd3751 100644
--- a/frontend/react/src/map/stylesheets/map.css
+++ b/frontend/react/src/map/stylesheets/map.css
@@ -141,6 +141,12 @@
width: 100%;
}
+.ol-spot-icon {
+ background-image: url("/images/markers/target.svg");
+ height: 100%;
+ width: 100%;
+}
+
.ol-text-icon {
color: #111111;
text-align: center;
diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts
index 4c87b105..6c22af15 100644
--- a/frontend/react/src/mission/missionmanager.ts
+++ b/frontend/react/src/mission/missionmanager.ts
@@ -3,14 +3,16 @@ import { getApp } from "../olympusapp";
import { Airbase } from "./airbase";
import { Bullseye } from "./bullseye";
import { BLUE_COMMANDER, GAME_MASTER, NONE, RED_COMMANDER } from "../constants/constants";
-import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData } from "../interfaces";
+import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData, SpotsData } from "../interfaces";
import { Coalition } from "../types/types";
import { Carrier } from "./carrier";
import { AirbaseSelectedEvent, AppStateChangedEvent, BullseyesDataChangedEvent, CommandModeOptionsChangedEvent, EnabledCommandModesChangedEvent, MissionDataChangedEvent } from "../events";
+import { Spot } from "./spot";
/** The MissionManager */
export class MissionManager {
#bullseyes: { [name: string]: Bullseye } = {};
+ #spots: {[key: string]: Spot} = {};
#airbases: { [name: string]: Airbase | Carrier } = {};
#theatre: string = "";
#dateAndTime: DateAndTime = {
@@ -65,6 +67,16 @@ export class MissionManager {
}
}
+ updateSpots(data: SpotsData) {
+ for (let idx in data.spots) {
+ const spotID = Number(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);
+ }
+ }
+ }
+
/** Update airbase information
*
* @param object
@@ -134,6 +146,10 @@ export class MissionManager {
return this.#airbases;
}
+ getSpots() {
+ return this.#spots;
+ }
+
/** Get the options/settings as set in the command mode
*
* @returns object
diff --git a/frontend/react/src/mission/spot.ts b/frontend/react/src/mission/spot.ts
new file mode 100644
index 00000000..2bd72624
--- /dev/null
+++ b/frontend/react/src/mission/spot.ts
@@ -0,0 +1,48 @@
+import { LatLng } from "leaflet";
+import { getApp } from "../olympusapp";
+
+export class Spot {
+ private ID: number;
+ private type: string;
+ private targetPosition: LatLng;
+ private sourceUnitID: number;
+ private code?: number;
+
+ constructor(ID: number, type: string, targetPosition: LatLng, sourceUnitID: number, code?: number) {
+ this.ID = ID;
+ this.type = type;
+ this.targetPosition = targetPosition;
+ this.sourceUnitID = sourceUnitID;
+ this.code = code;
+ }
+
+ // Getter methods
+ getID() {
+ return this.ID;
+ }
+
+ getType() {
+ return this.type;
+ }
+
+ getTargetPosition() {
+ return this.targetPosition;
+ }
+
+ getSourceUnitID() {
+ return this.sourceUnitID;
+ }
+
+ getCode() {
+ return this.code;
+ }
+
+ // Setter methods
+ setTargetPosition(position: LatLng) {
+ this.targetPosition = position;
+ }
+
+ setCode(code: number) {
+ this.code = code;
+ }
+}
\ No newline at end of file
diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts
index d393ba44..dd37bdcf 100644
--- a/frontend/react/src/server/servermanager.ts
+++ b/frontend/react/src/server/servermanager.ts
@@ -8,12 +8,13 @@ import {
MISSION_URI,
NONE,
ROEs,
+ SPOTS_URI,
UNITS_URI,
WEAPONS_URI,
emissionsCountermeasures,
reactionsToThreat,
} from "../constants/constants";
-import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, 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 {
@@ -180,10 +181,14 @@ export class ServerManager {
this.GET(callback, errorCallback, AIRBASES_URI);
}
- getBullseye(callback: CallableFunction, errorCallback: CallableFunction = () => {}) {
+ getBullseyes(callback: CallableFunction, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, BULLSEYE_URI);
}
+ getSpots(callback: CallableFunction, errorCallback: CallableFunction = () => {}) {
+ this.GET(callback, errorCallback, SPOTS_URI);
+ }
+
getLogs(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, LOGS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[LOGS_URI] }, "text", refresh);
}
@@ -576,7 +581,7 @@ export class ServerManager {
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
- this.getBullseye((data: BullseyesData) => {
+ this.getBullseyes((data: BullseyesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateBullseyes(data);
return data.time;
@@ -585,6 +590,18 @@ export class ServerManager {
}, 10000)
);
+ this.#intervals.push(
+ window.setInterval(() => {
+ if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
+ this.getSpots((data: SpotsData) => {
+ this.checkSessionHash(data.sessionHash);
+ getApp().getMissionManager()?.updateSpots(data);
+ return data.time;
+ });
+ }
+ }, 2000)
+ );
+
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
@@ -670,7 +687,7 @@ export class ServerManager {
return data.time;
});
- this.getBullseye((data: BullseyesData) => {
+ this.getBullseyes((data: BullseyesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateBullseyes(data);
return data.time;
diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx
index 41362f75..54a8acbb 100644
--- a/frontend/react/src/ui/panels/header.tsx
+++ b/frontend/react/src/ui/panels/header.tsx
@@ -17,7 +17,7 @@ import {
} from "../components/olicons";
import { FaChevronLeft, FaChevronRight, FaFloppyDisk } from "react-icons/fa6";
import { CommandModeOptionsChangedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent, SessionDataChangedEvent, SessionDataSavedEvent } from "../../events";
-import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, ImportExportSubstate, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, OlympusState } from "../../constants/constants";
+import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, ImportExportSubstate, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, OlympusState, RED_COMMANDER } from "../../constants/constants";
import { OlympusConfig } from "../../interfaces";
import { FaCheck, FaSave, FaSpinner } from "react-icons/fa";
@@ -132,6 +132,11 @@ export function Header() {
BLUE Commander ({commandModeOptions.spawnPoints.blue} points)
)}
+ {commandModeOptions.commandMode === RED_COMMANDER && (
+
+ BLUE Commander ({commandModeOptions.spawnPoints.blue} points)
+
+ )}
{
+ 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());
+ }
+ this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
+ this.#spotEditMarkers[spot.getID()].setTextValue(`${spot.getCode() ?? ""}`);
+ }
+ });
+ }
+
+ #clearSpots() {
+ Object.values(this.#spots).forEach((spot) => getApp().getMap().removeLayer(spot));
+ Object.values(this.#spotEditMarkers).forEach((spotEditMarker) => getApp().getMap().removeLayer(spotEditMarker));
+ Object.values(this.#spotMarkers).forEach((spotMarker) => getApp().getMap().removeLayer(spotMarker));
+ }
+
#drawContacts() {
this.#clearContacts();
if (getApp().getMap().getOptions().showUnitContacts) {
diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua
index a14512bb..6aba3015 100644
--- a/scripts/lua/backend/OlympusCommand.lua
+++ b/scripts/lua/backend/OlympusCommand.lua
@@ -602,6 +602,7 @@ function Olympus.fireInfrared(ID, lat, lng)
local unit = Olympus.getUnitByID(ID)
if unit ~= nil and unit:isExist() then
local spot = Spot.createInfraRed(unit, {x = 0, y = 1, z = 0}, vec3)
+ Olympus.spotsCounter = Olympus.spotsCounter + 1
Olympus.spots[Olympus.spotsCounter] = {
type = "infrared",
object = spot,