diff --git a/backend/core/include/datatypes.h b/backend/core/include/datatypes.h index 471b288a..bd99cd6c 100644 --- a/backend/core/include/datatypes.h +++ b/backend/core/include/datatypes.h @@ -51,6 +51,9 @@ namespace DataIndex { shotsScatter, shotsIntensity, health, + racetrackLength, + racetrackAnchor, + racetrackBearing, lastIndex, endOfData = 255 }; diff --git a/backend/core/include/unit.h b/backend/core/include/unit.h index 83f39c4b..7bbc1397 100644 --- a/backend/core/include/unit.h +++ b/backend/core/include/unit.h @@ -109,6 +109,9 @@ public: virtual void setShotsScatter(unsigned char newValue) { updateValue(shotsScatter, newValue, DataIndex::shotsScatter); } virtual void setShotsIntensity(unsigned char newValue) { updateValue(shotsIntensity, newValue, DataIndex::shotsIntensity); } virtual void setHealth(unsigned char newValue) { updateValue(health, newValue, DataIndex::health); } + virtual void setRacetrackLength(double newValue) { updateValue(racetrackLength, newValue, DataIndex::racetrackLength); } + virtual void setRacetrackAnchor(Coords newValue) { updateValue(racetrackAnchor, newValue, DataIndex::racetrackAnchor); } + virtual void setRacetrackBearing(double newValue) { updateValue(racetrackBearing, newValue, DataIndex::racetrackBearing); } /********** Getters **********/ virtual string getCategory() { return category; }; @@ -157,6 +160,9 @@ public: virtual unsigned char getShotsScatter() { return shotsScatter; } virtual unsigned char getShotsIntensity() { return shotsIntensity; } virtual unsigned char getHealth() { return health; } + virtual double getRacetrackLength() { return racetrackLength; } + virtual Coords getRacetrackAnchor() { return racetrackAnchor; } + virtual double getRacetrackBearing() { return racetrackBearing; } protected: unsigned int ID; @@ -190,6 +196,9 @@ protected: double desiredAltitude = 1; bool desiredAltitudeType = 0; /* ASL */ unsigned int leaderID = NULL; + double racetrackLength = NULL; + Coords racetrackAnchor = Coords(NULL); + double racetrackBearing = NULL; Offset formationOffset = Offset(NULL); unsigned int targetID = NULL; Coords targetPosition = Coords(NULL); diff --git a/backend/core/src/airunit.cpp b/backend/core/src/airunit.cpp index 703ba7ad..99f85fb5 100644 --- a/backend/core/src/airunit.cpp +++ b/backend/core/src/airunit.cpp @@ -154,6 +154,11 @@ void AirUnit::AIloop() { srand(static_cast(time(NULL)) + ID); + if (state != State::IDLE) { + setRacetrackAnchor(Coords(NULL)); + setRacetrackBearing(NULL); + } + /* State machine */ switch (state) { case State::IDLE: { @@ -166,23 +171,27 @@ void AirUnit::AIloop() if (!getHasTask()) { + if (racetrackAnchor == Coords(NULL)) setRacetrackAnchor(position); + if (racetrackBearing == NULL) setRacetrackBearing(heading); + std::ostringstream taskSS; if (isActiveTanker) { taskSS << "{ [1] = { id = 'Tanker' }, [2] = { id = 'Orbit', pattern = 'Race-Track', altitude = " << - desiredAltitude << ", speed = " << desiredSpeed << ", altitudeType = '" << + desiredAltitude << ", lat = " << racetrackAnchor.lat << ", lng = " << racetrackAnchor.lng << ", speed = " << desiredSpeed << ", altitudeType = '" << (desiredAltitudeType ? "AGL" : "ASL") << "', speedType = '" << (desiredSpeedType ? "GS" : "CAS") << "', heading = " << - heading << ", length = " << (50000 * 1.852) << " }}"; + racetrackBearing << ", length = " << (racetrackLength != NULL ? racetrackLength : (50000 * 1.852)) << " }}"; } else if (isActiveAWACS) { - taskSS << "{ [1] = { id = 'AWACS' }, [2] = { id = 'Orbit', pattern = 'Circle', altitude = " << - desiredAltitude << ", speed = " << desiredSpeed << ", altitudeType = '" << - (desiredAltitudeType ? "AGL" : "ASL") << "', speedType = '" << (desiredSpeedType ? "GS" : "CAS") << "' }}"; + taskSS << "{ [1] = { id = 'AWACS' }, [2] = { id = 'Orbit', pattern = 'Race-Track', altitude = " << + desiredAltitude << ", lat = " << racetrackAnchor.lat << ", lng = " << racetrackAnchor.lng << ", speed = " << desiredSpeed << ", altitudeType = '" << + (desiredAltitudeType ? "AGL" : "ASL") << "', speedType = '" << (desiredSpeedType ? "GS" : "CAS") << "', heading = " << + racetrackBearing << ", length = " << (racetrackLength != NULL ? racetrackLength : (desiredSpeed * 30)) << " }}"; } else { taskSS << "{ id = 'Orbit', pattern = 'Race-Track', altitude = " << - desiredAltitude << ", speed = " << desiredSpeed << ", altitudeType = '" << + desiredAltitude << ", lat = " << racetrackAnchor.lat << ", lng = " << racetrackAnchor.lng << ", speed = " << desiredSpeed << ", altitudeType = '" << (desiredAltitudeType ? "AGL" : "ASL") << "', speedType = '" << (desiredSpeedType ? "GS" : "CAS") << "', heading = " << - heading << ", length = " << desiredSpeed * 30 << " }"; + racetrackBearing << ", length = " << (racetrackLength != NULL ? racetrackLength: (desiredSpeed * 30)) << " }"; } Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); scheduler->appendCommand(command); diff --git a/backend/core/src/scheduler.cpp b/backend/core/src/scheduler.cpp index f2c75fd4..b92953f1 100644 --- a/backend/core/src/scheduler.cpp +++ b/backend/core/src/scheduler.cpp @@ -353,6 +353,16 @@ void Scheduler::handleRequest(string key, json::value value, string username, js unit->setDesiredAltitudeType(to_string(value[L"altitudeType"])); log(username + " set " + unit->getUnitName() + "(" + unit->getName() + ") altitude type: " + to_string(value[L"altitudeType"]), true); } + }/************************/ + else if (key.compare("setRacetrackLength") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) { + unit->setRacetrackLength(value[L"racetrackLength"].as_double()); + log(username + " set " + unit->getUnitName() + "(" + unit->getName() + ") racetrack length: " + to_string(value[L"racetrackLength"].as_double()), true); + } } /************************/ else if (key.compare("cloneUnits") == 0) diff --git a/backend/core/src/unit.cpp b/backend/core/src/unit.cpp index 0ad51970..480e5242 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -295,6 +295,9 @@ void Unit::getData(stringstream& ss, unsigned long long time) case DataIndex::shotsScatter: appendNumeric(ss, datumIndex, shotsScatter); break; case DataIndex::shotsIntensity: appendNumeric(ss, datumIndex, shotsIntensity); break; case DataIndex::health: appendNumeric(ss, datumIndex, health); break; + case DataIndex::racetrackLength: appendNumeric(ss, datumIndex, racetrackLength); break; + case DataIndex::racetrackAnchor: appendNumeric(ss, datumIndex, racetrackAnchor); break; + case DataIndex::racetrackBearing: appendNumeric(ss, datumIndex, racetrackBearing); break; } } } diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 2aaca9c0..cb919638 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -484,6 +484,9 @@ export enum DataIndexes { shotsScatter, shotsIntensity, health, + racetrackLength, + racetrackAnchor, + racetrackBearing, endOfData = 255, } diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index a70794c9..0515da63 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -248,6 +248,9 @@ export interface UnitData { shotsScatter: number; shotsIntensity: number; health: number; + racetrackLength: number; + racetrackAnchor: LatLng; + racetrackBearing: number; } export interface LoadoutItemBlueprint { diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 70233bac..986a6ef0 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -555,8 +555,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { selectedUnits.forEach((unit) => { unit.setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL"); @@ -617,8 +617,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {!(everyUnitIsGround || everyUnitIsNavy) && ( { selectedUnits.forEach((unit) => { unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS"); diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 669b9e56..a8feb9fa 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -1,4 +1,4 @@ -import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point, LeafletMouseEvent, DomEvent, DomUtil } from "leaflet"; +import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point, LeafletMouseEvent, DomEvent, DomUtil, Circle } from "leaflet"; import { getApp } from "../olympusapp"; import { enumToCoalition, @@ -17,6 +17,8 @@ import { zeroAppend, computeBearingRangeString, adjustBrightness, + bearingAndDistanceToLatLng, + mToNm, } from "../other/utils"; import { CustomMarker } from "../map/markers/custommarker"; import { SVGInjector } from "@tanem/svg-injector"; @@ -145,6 +147,9 @@ export abstract class Unit extends CustomMarker { #shotsScatter: number = 2; #shotsIntensity: number = 2; #health: number = 100; + #racetrackLength: number = 0; + #racetrackAnchor: LatLng = new LatLng(0, 0); + #racetrackBearing: number = 0; /* Other members used to draw the unit, mostly ancillary stuff like targets, ranges and so on */ #blueprint: UnitBlueprint | null = null; @@ -166,6 +171,9 @@ export abstract class Unit extends CustomMarker { #detectionMethods: number[] = []; #trailPositions: LatLng[] = []; #trailPolylines: Polyline[] = []; + #racetrackPolylines: Polyline[] = [new Polyline([]), new Polyline([])]; + #racetrackArcs: Polyline[] = [new Polyline([]), new Polyline([])]; + #anchorMarkers: Marker[]; /* Inputs timers */ #debounceTimeout: number | null = null; @@ -312,6 +320,15 @@ export abstract class Unit extends CustomMarker { getHealth() { return this.#health; } + getRaceTrackLength() { + return this.#racetrackLength; + } + getRaceTrackAnchor() { + return this.#racetrackAnchor; + } + getRaceTrackBearing() { + return this.#racetrackBearing; + } static getConstructor(type: string) { if (type === "GroundUnit") return GroundUnit; @@ -326,7 +343,7 @@ export abstract class Unit extends CustomMarker { this.ID = ID; this.#pathPolyline = new Polyline([], { - color: colors.GRAY, + color: colors.STEEL_BLUE, weight: 3, opacity: 0.5, smoothFactor: 1, @@ -588,6 +605,15 @@ export abstract class Unit extends CustomMarker { this.#health = dataExtractor.extractUInt8(); updateMarker = true; break; + case DataIndexes.racetrackLength: + this.#racetrackLength = dataExtractor.extractFloat64(); + break; + case DataIndexes.racetrackAnchor: + this.#racetrackAnchor = dataExtractor.extractLatLng(); + break; + case DataIndexes.racetrackBearing: + this.#racetrackBearing = dataExtractor.extractFloat64(); + break; } } @@ -691,6 +717,9 @@ export abstract class Unit extends CustomMarker { shotsScatter: this.#shotsScatter, shotsIntensity: this.#shotsIntensity, health: this.#health, + racetrackLength: this.#racetrackLength, + racetrackAnchor: this.#racetrackAnchor, + racetrackBearing: this.#racetrackBearing, }; } @@ -719,6 +748,7 @@ export abstract class Unit extends CustomMarker { this.#clearContacts(); this.#clearPath(); this.#clearTargetPosition(); + this.#clearRacetrack(); } /* When the group leader is selected, if grouping is active, all the other group members are also selected */ @@ -861,6 +891,7 @@ export abstract class Unit extends CustomMarker { /* Leaflet does not like it when you change coordinates when the map is zooming */ if (!getApp().getMap().isZooming()) { this.#drawPath(); + this.#drawRacetrack(); this.#drawContacts(); this.#drawTarget(); } @@ -893,7 +924,7 @@ export abstract class Unit extends CustomMarker { el.classList.add("unit"); el.setAttribute("data-object", `unit-${this.getMarkerCategory()}`); el.setAttribute("data-coalition", this.#coalition); - + var iconOptions = this.getIconOptions(); /* Generate and append elements depending on active options */ @@ -1569,12 +1600,14 @@ export abstract class Unit extends CustomMarker { } else if (element.querySelector(".unit-braa")) (element.querySelector(".unit-braa")).innerText = ``; /* Set operate as */ - element.querySelector(".unit")?.setAttribute( - "data-operate-as", - this.getState() === UnitState.MISS_ON_PURPOSE || this.getState() === UnitState.SCENIC_AAA || this.getState() === UnitState.SIMULATE_FIRE_FIGHT - ? this.#operateAs - : "neutral" - ); + element + .querySelector(".unit") + ?.setAttribute( + "data-operate-as", + this.getState() === UnitState.MISS_ON_PURPOSE || this.getState() === UnitState.SCENIC_AAA || this.getState() === UnitState.SIMULATE_FIRE_FIGHT + ? this.#operateAs + : "neutral" + ); } /* Set vertical offset for altitude stacking */ @@ -1677,6 +1710,67 @@ export abstract class Unit extends CustomMarker { } } + #drawRacetrack() { + let groundspeed = this.#speed; + + let racetrackLength = this.#racetrackLength; + if (racetrackLength === 0) { + if (this.getIsActiveTanker()) + racetrackLength = nmToM(50); + else + racetrackLength = this.#desiredSpeed * 30; + } + const radius = Math.pow(groundspeed, 2) / 9.81 / Math.tan(deg2rad(22.5)); + const point1 = this.#racetrackAnchor; + const point2 = bearingAndDistanceToLatLng(point1.lat, point1.lng, this.#racetrackBearing, racetrackLength); + const point3 = bearingAndDistanceToLatLng(point2.lat, point2.lng, this.#racetrackBearing - deg2rad(90), radius * 2); + const point4 = bearingAndDistanceToLatLng(point1.lat, point1.lng, this.#racetrackBearing - deg2rad(90), radius * 2); + + const center1 = bearingAndDistanceToLatLng(point2.lat, point2.lng, this.#racetrackBearing - deg2rad(90), radius); + const center2 = bearingAndDistanceToLatLng(point1.lat, point1.lng, this.#racetrackBearing - deg2rad(90), radius); + + if (!getApp().getMap().hasLayer(this.#racetrackPolylines[0])) { + this.#racetrackPolylines[0] = new Polyline([point1, point2]); + this.#racetrackPolylines[0].addTo(getApp().getMap()); + } else { + this.#racetrackPolylines[0].setLatLngs([point1, point2]); + } + + if (!getApp().getMap().hasLayer(this.#racetrackPolylines[1])) { + this.#racetrackPolylines[1] = new Polyline([point3, point4]); + this.#racetrackPolylines[1].addTo(getApp().getMap()); + } else { + this.#racetrackPolylines[1].setLatLngs([point3, point4]); + } + + const arc1Points: LatLng[] = []; + const arc2Points: LatLng[] = []; + + for (let theta = 0; theta <= 180; theta += 5) { + arc1Points.push(bearingAndDistanceToLatLng(center1.lat, center1.lng, this.#racetrackBearing + deg2rad(theta - 90), radius)); + arc2Points.push(bearingAndDistanceToLatLng(center2.lat, center2.lng, this.#racetrackBearing + deg2rad(theta + 90), radius)); + } + + if (!getApp().getMap().hasLayer(this.#racetrackArcs[0])) { + this.#racetrackArcs[0] = new Polyline(arc1Points); + this.#racetrackArcs[0].addTo(getApp().getMap()); + } else { + this.#racetrackArcs[0].setLatLngs(arc1Points); + } + + if (!getApp().getMap().hasLayer(this.#racetrackArcs[1])) { + this.#racetrackArcs[1] = new Polyline(arc2Points); + this.#racetrackArcs[1].addTo(getApp().getMap()); + } else { + this.#racetrackArcs[1].setLatLngs(arc2Points); + } + } + + #clearRacetrack() { + this.#racetrackPolylines.forEach((polyline) => getApp().getMap().removeLayer(polyline)); + this.#racetrackArcs.forEach((arc) => getApp().getMap().removeLayer(arc)); + } + #drawContacts() { this.#clearContacts(); if (getApp().getMap().getOptions().showUnitContacts) {