From cc902aec04a7e75e2e800d260addd998d19a6b8d Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Wed, 29 Jan 2025 17:25:09 +0100 Subject: [PATCH] feat: started adding editable laser/infrared --- backend/core/src/commands.cpp | 4 +- backend/core/src/server.cpp | 3 + backend/shared/include/defines.h | 1 + frontend/react/src/constants/constants.ts | 1 + frontend/react/src/interfaces.ts | 8 + frontend/react/src/map/map.ts | 1 + .../react/src/map/markers/spoteditmarker.ts | 216 ++++++++++++++++++ frontend/react/src/map/markers/spotmarker.ts | 23 ++ .../src/map/markers/stylesheets/spot.css | 90 ++++++++ frontend/react/src/map/stylesheets/map.css | 6 + frontend/react/src/mission/missionmanager.ts | 18 +- frontend/react/src/mission/spot.ts | 48 ++++ frontend/react/src/server/servermanager.ts | 25 +- frontend/react/src/ui/panels/header.tsx | 7 +- frontend/react/src/unit/unit.ts | 48 +++- scripts/lua/backend/OlympusCommand.lua | 1 + 16 files changed, 490 insertions(+), 10 deletions(-) create mode 100644 frontend/react/src/map/markers/spoteditmarker.ts create mode 100644 frontend/react/src/map/markers/spotmarker.ts create mode 100644 frontend/react/src/map/markers/stylesheets/spot.css create mode 100644 frontend/react/src/mission/spot.ts 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: `
+ +
${textValue}
+
X
+
`, + }), + }); + + 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,