feat: started adding editable laser/infrared

This commit is contained in:
Davide Passoni 2025-01-29 17:25:09 +01:00
parent 74e2332b17
commit cc902aec04
16 changed files with 490 additions and 10 deletions

View File

@ -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;

View File

@ -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"];

View File

@ -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"

View File

@ -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";

View File

@ -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;

View File

@ -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";

View File

@ -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: `<div class="container">
<input class="input"/>
<div class="text">${textValue}</div>
<div class="delete">X</div>
</div>`,
}),
});
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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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 <AirbasesData>
@ -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

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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() {
<span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span>
</div>
)}
{commandModeOptions.commandMode === RED_COMMANDER && (
<div className={`flex h-full rounded-md bg-red-600 px-4 text-white`}>
<span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span>
</div>
)}
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
<OlLockStateButton
checked={!mapOptions.protectDCSUnits}

View File

@ -68,6 +68,9 @@ import {
} from "../events";
import { CoalitionAreaHandle } from "../map/coalitionarea/coalitionareahandle";
import { ArrowMarker } from "../map/markers/arrowmarker";
import { Spot } from "../mission/spot";
import { SpotEditMarker } from "../map/markers/spoteditmarker";
import { SpotMarker } from "../map/markers/spotmarker";
const bearingStrings = ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west", "north"];
@ -177,6 +180,9 @@ 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 } = {};
#spotEditMarkers: { [key: number]: SpotEditMarker } = {};
#spotMarkers: { [key: number]: SpotMarker } = {};
/* Inputs timers */
#debounceTimeout: number | null = null;
@ -779,6 +785,7 @@ export abstract class Unit extends CustomMarker {
this.#clearPath();
this.#clearTargetPosition();
this.#clearRacetrack();
this.#clearSpots();
}
/* When the group leader is selected, if grouping is active, all the other group members are also selected */
@ -928,6 +935,7 @@ export abstract class Unit extends CustomMarker {
this.#drawRacetrack();
this.#drawContacts();
this.#drawTarget();
this.#drawSpots();
}
}
@ -1740,8 +1748,7 @@ export abstract class Unit extends CustomMarker {
}
/* Add racetrack anchor to the path, but only if we are an active tanker or AWACS */
if (this.getState() !== UnitState.IDLE && (this.getIsActiveAWACS() || this.getIsActiveTanker()))
points.push(this.#racetrackAnchor);
if (this.getState() !== UnitState.IDLE && (this.getIsActiveAWACS() || this.getIsActiveTanker())) points.push(this.#racetrackAnchor);
this.#pathPolyline.setLatLngs(points);
@ -1859,6 +1866,43 @@ export abstract class Unit extends CustomMarker {
});
}
#drawSpots() {
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());
}
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) {

View File

@ -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,