feat(radar state): initial scaffolding

This commit is contained in:
MarcoJayUsai 2025-03-18 14:42:46 +01:00
parent 16e77087f5
commit ee15106be1
8 changed files with 244 additions and 133 deletions

View File

@ -7,6 +7,7 @@ namespace DataIndex {
startOfData = 0,
category,
alive,
radarState,
human,
controlled,
coalition,

View File

@ -112,10 +112,12 @@ public:
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); }
virtual void setRadarState(string newValue) { updateValue(radarState, newValue, DataIndex::radarState); }
/********** Getters **********/
virtual string getCategory() { return category; };
virtual bool getAlive() { return alive; }
virtual string getRadarState() { return radarState; }
virtual bool getHuman() { return human; }
virtual bool getControlled() { return controlled; }
virtual unsigned char getCoalition() { return coalition; }
@ -178,6 +180,7 @@ protected:
string callsign = "";
string groupName = "";
unsigned char state = State::NONE;
string radarState = "";
string task = "";
bool hasTask = false;
Coords position = Coords(NULL);

View File

@ -152,7 +152,6 @@ void Server::handle_get(http_request request)
}
/* Drawings data*/
else if (URI.compare(DRAWINGS_URI) == 0 && drawingsByLayer.has_object_field(L"drawings")) {
log("Trying to answer with drawings...");
answer[L"drawings"] = drawingsByLayer[L"drawings"];
}

View File

@ -83,6 +83,11 @@ void Unit::update(json::value json, double dt)
if (json.has_boolean_field(L"isAlive"))
setAlive(json[L"isAlive"].as_bool());
if (json.has_string_field(L"radarState")) {
log("Unit " + to_string(json[L"unitName"]) + " has radarState: " + to_string(json[L"radarState"]));
setRadarState(to_string(json[L"radarState"]));
}
if (json.has_boolean_field(L"isHuman"))
setHuman(json[L"isHuman"].as_bool());
@ -208,6 +213,7 @@ void Unit::refreshLeaderData(unsigned long long time) {
case DataIndex::operateAs: updateValue(operateAs, leader->operateAs, datumIndex); break;
case DataIndex::shotsScatter: updateValue(shotsScatter, leader->shotsScatter, datumIndex); break;
case DataIndex::shotsIntensity: updateValue(shotsIntensity, leader->shotsIntensity, datumIndex); break;
case DataIndex::radarState: updateValue(radarState, leader->radarState, datumIndex); break;
}
}
}
@ -251,6 +257,7 @@ void Unit::getData(stringstream& ss, unsigned long long time)
switch (datumIndex) {
case DataIndex::category: appendString(ss, datumIndex, category); break;
case DataIndex::alive: appendNumeric(ss, datumIndex, alive); break;
case DataIndex::radarState: appendNumeric(ss, datumIndex, radarState); break;
case DataIndex::human: appendNumeric(ss, datumIndex, human); break;
case DataIndex::controlled: appendNumeric(ss, datumIndex, controlled); break;
case DataIndex::coalition: appendNumeric(ss, datumIndex, coalition); break;

View File

@ -96,6 +96,12 @@ export const states: string[] = [
UnitState.LAND_AT_POINT,
];
export enum RADAR_STATES {
RED = 'red',
GREEN = 'green',
AUTO = 'auto'
}
export const ROEs: string[] = ["free", "designated", "", "return", "hold"];
export const reactionsToThreat: string[] = ["none", "manoeuvre", "passive", "evade"];
export const emissionsCountermeasures: string[] = ["silent", "attack", "defend", "free"];
@ -448,6 +454,7 @@ export enum DataIndexes {
startOfData = 0,
category,
alive,
radarState,
human,
controlled,
coalition,

View File

@ -83,6 +83,7 @@ export abstract class Unit extends CustomMarker {
/* Data controlled directly by the backend. No setters are provided to avoid misalignments */
#alive: boolean = false;
#radarState: string = '';
#human: boolean = false;
#controlled: boolean = false;
#coalition: string = "neutral";
@ -488,6 +489,10 @@ export abstract class Unit extends CustomMarker {
this.setAlive(dataExtractor.extractBool());
updateMarker = true;
break;
case DataIndexes.radarState:
this.setRadarState(dataExtractor.extractString());
updateMarker = true;
break;
case DataIndexes.human:
this.#human = dataExtractor.extractBool();
break;
@ -764,6 +769,15 @@ export abstract class Unit extends CustomMarker {
}
}
setRadarState(newRadarState: string) {
if (newRadarState != this.#radarState) {
this.#radarState = newRadarState;
// TODO: check if an event is needed -- surely yes to update the UI
console.log('----radar state updated: ', this.#radarState);
this.#updateMarker();
}
}
/** Set the unit as user-selected
*
* @param selected (boolean)
@ -1542,149 +1556,152 @@ export abstract class Unit extends CustomMarker {
}
}
if (this.getHidden()) return; // We won't draw the marker if the unit is hidden
/* Draw the marker */
if (!this.getHidden()) {
if (this.getLatLng().lat !== this.#position.lat || this.getLatLng().lng !== this.#position.lng) {
this.setLatLng(new LatLng(this.#position.lat, this.#position.lng));
}
if (this.getLatLng().lat !== this.#position.lat || this.getLatLng().lng !== this.#position.lng) {
this.setLatLng(new LatLng(this.#position.lat, this.#position.lng));
}
var element = this.getElement();
if (element != null) {
/* Draw the velocity vector */
element.querySelector(".unit-vvi")?.setAttribute("style", `height: ${15 + this.#speed / 5}px;`);
var element = this.getElement();
if (element != null) {
/* Draw the velocity vector */
element.querySelector(".unit-vvi")?.setAttribute("style", `height: ${15 + this.#speed / 5}px;`);
/* Set fuel data */
element.querySelector(".unit-fuel-level")?.setAttribute("style", `width: ${this.#fuel}%`);
element.querySelector(".unit")?.toggleAttribute("data-has-low-fuel", this.#fuel < 20);
/* Set fuel data */
element.querySelector(".unit-fuel-level")?.setAttribute("style", `width: ${this.#fuel}%`);
element.querySelector(".unit")?.toggleAttribute("data-has-low-fuel", this.#fuel < 20);
/* Set health data */
element.querySelector(".unit-health-level")?.setAttribute("style", `width: ${this.#health}%`);
element.querySelector(".unit")?.toggleAttribute("data-has-low-health", this.#health < 20);
/* Set health data */
element.querySelector(".unit-health-level")?.setAttribute("style", `width: ${this.#health}%`);
element.querySelector(".unit")?.toggleAttribute("data-has-low-health", this.#health < 20);
/* Set dead/alive flag */
element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.#alive);
/* Set dead/alive flag */
element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.#alive);
/* Set current unit state */
if (this.#human) {
// Unit is human
element.querySelector(".unit")?.setAttribute("data-state", "human");
} else if (!this.#controlled) {
// Unit is under DCS control (not Olympus)
element.querySelector(".unit")?.setAttribute("data-state", "dcs");
} else if ((this.getCategory() == "Aircraft" || this.getCategory() == "Helicopter") && !this.#hasTask) {
element.querySelector(".unit")?.setAttribute("data-state", "no-task");
} else {
// Unit is under Olympus control
if (this.#onOff) {
if (this.#isActiveTanker) element.querySelector(".unit")?.setAttribute("data-state", "tanker");
else if (this.#isActiveAWACS) element.querySelector(".unit")?.setAttribute("data-state", "AWACS");
else element.querySelector(".unit")?.setAttribute("data-state", this.#state.toLowerCase());
} else {
element.querySelector(".unit")?.setAttribute("data-state", "off");
}
}
/* Set RED/GREEN state*/
if (this.#radarState !== '') element.querySelector(".unit")?.setAttribute("data-radar-state", this.#radarState);
/* Set altitude and speed */
if (element.querySelector(".unit-altitude"))
(<HTMLElement>element.querySelector(".unit-altitude")).innerText = "FL" + zeroAppend(Math.floor(mToFt(this.#position.alt as number) / 100), 3);
if (element.querySelector(".unit-speed"))
(<HTMLElement>element.querySelector(".unit-speed")).innerText = String(Math.floor(msToKnots(this.#speed))) + "GS";
/* Rotate elements according to heading */
element.querySelectorAll("[data-rotate-to-heading]").forEach((el) => {
const headingDeg = rad2deg(this.#track);
let currentStyle = el.getAttribute("style") || "";
el.setAttribute("style", currentStyle + `transform:rotate(${headingDeg}deg);`);
});
/* Turn on ammo indicators */
var hasFox1 = element.querySelector(".unit")?.hasAttribute("data-has-fox-1");
var hasFox2 = element.querySelector(".unit")?.hasAttribute("data-has-fox-2");
var hasFox3 = element.querySelector(".unit")?.hasAttribute("data-has-fox-3");
var hasOtherAmmo = element.querySelector(".unit")?.hasAttribute("data-has-other-ammo");
var newHasFox1 = false;
var newHasFox2 = false;
var newHasFox3 = false;
var newHasOtherAmmo = false;
Object.values(this.#ammo).forEach((ammo: Ammo) => {
if (ammo.category == 1 && ammo.missileCategory == 1) {
if (ammo.guidance == 4 || ammo.guidance == 5) newHasFox1 = true;
else if (ammo.guidance == 2) newHasFox2 = true;
else if (ammo.guidance == 3) newHasFox3 = true;
} else newHasOtherAmmo = true;
});
if (hasFox1 != newHasFox1) element.querySelector(".unit")?.toggleAttribute("data-has-fox-1", newHasFox1);
if (hasFox2 != newHasFox2) element.querySelector(".unit")?.toggleAttribute("data-has-fox-2", newHasFox2);
if (hasFox3 != newHasFox3) element.querySelector(".unit")?.toggleAttribute("data-has-fox-3", newHasFox3);
if (hasOtherAmmo != newHasOtherAmmo) element.querySelector(".unit")?.toggleAttribute("data-has-other-ammo", newHasOtherAmmo);
/* Draw the hotgroup element */
element.querySelector(".unit")?.toggleAttribute("data-is-in-hotgroup", this.#hotgroup != null);
if (this.#hotgroup) {
const hotgroupEl = element.querySelector(".unit-hotgroup-id") as HTMLElement;
if (hotgroupEl) hotgroupEl.innerText = String(this.#hotgroup);
}
/* Set bullseyes positions */
const bullseyes = getApp().getMissionManager().getBullseyes();
if (Object.keys(bullseyes).length > 0) {
const bullseye = `${computeBearingRangeString(bullseyes[coalitionToEnum(getApp().getMap().getOptions().AWACSCoalition)].getLatLng(), this.getPosition())}`;
if (element.querySelector(".unit-bullseye")) (<HTMLElement>element.querySelector(".unit-bullseye")).innerText = `${bullseye}`;
}
/* Set BRAA */
const reference = getApp().getUnitsManager().getAWACSReference();
if (reference && reference !== this && reference.getAlive()) {
const BRAA = `${computeBearingRangeString(reference.getPosition(), this.getPosition())}`;
if (element.querySelector(".unit-braa")) (<HTMLElement>element.querySelector(".unit-braa")).innerText = `${BRAA}`;
} else if (element.querySelector(".unit-braa")) (<HTMLElement>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"
);
}
/* Set vertical offset for altitude stacking */
var pos = getApp().getMap().latLngToLayerPoint(this.getLatLng()).round();
this.setZIndexOffset(1000 + Math.floor(this.#position.alt as number) - pos.y + (this.#highlighted || this.#selected ? 5000 : 0));
/* Get the cluster this unit is in to position the label correctly */
let cluster = Object.values(getApp().getUnitsManager().getClusters()).find((cluster) => cluster.includes(this));
if (cluster && cluster.length > 1) {
let clusterMean = turf.centroid(turf.featureCollection(cluster.map((unit) => turf.point([unit.getPosition().lng, unit.getPosition().lat]))));
let bearingFromCluster = bearing(
clusterMean.geometry.coordinates[1],
clusterMean.geometry.coordinates[0],
this.getPosition().lat,
this.getPosition().lng,
false
);
if (bearingFromCluster < 0) bearingFromCluster += 360;
let trackIndex = Math.round(bearingFromCluster / 45);
for (let idx = 0; idx < bearingStrings.length; idx++) element?.querySelector(".unit-summary")?.classList.remove("cluster-" + bearingStrings[idx]);
element?.querySelector(".unit-summary")?.classList.add("cluster-" + bearingStrings[trackIndex]);
/* Set current unit state */
if (this.#human) {
// Unit is human
element.querySelector(".unit")?.setAttribute("data-state", "human");
} else if (!this.#controlled) {
// Unit is under DCS control (not Olympus)
element.querySelector(".unit")?.setAttribute("data-state", "dcs");
} else if ((this.getCategory() == "Aircraft" || this.getCategory() == "Helicopter") && !this.#hasTask) {
element.querySelector(".unit")?.setAttribute("data-state", "no-task");
} else {
for (let idx = 0; idx < bearingStrings.length; idx++) element?.querySelector(".unit-summary")?.classList.remove("cluster-" + bearingStrings[idx]);
element?.querySelector(".unit-summary")?.classList.add("cluster-north-east");
// Unit is under Olympus control
if (this.#onOff) {
if (this.#isActiveTanker) element.querySelector(".unit")?.setAttribute("data-state", "tanker");
else if (this.#isActiveAWACS) element.querySelector(".unit")?.setAttribute("data-state", "AWACS");
else element.querySelector(".unit")?.setAttribute("data-state", this.#state.toLowerCase());
} else {
element.querySelector(".unit")?.setAttribute("data-state", "off");
}
}
/* Draw the contact trail */
if (/*TODO getApp().getMap().getOptions().AWACSMode*/ false) {
this.#trailPolylines = this.#trailPositions.map(
(latlng, idx) => new Polyline([latlng, latlng], { color: colors.WHITE, opacity: 1 - (idx + 1) / TRAIL_LENGTH })
);
this.#trailPolylines.forEach((polyline) => polyline.addTo(getApp().getMap()));
/* Set altitude and speed */
if (element.querySelector(".unit-altitude"))
(<HTMLElement>element.querySelector(".unit-altitude")).innerText = "FL" + zeroAppend(Math.floor(mToFt(this.#position.alt as number) / 100), 3);
if (element.querySelector(".unit-speed"))
(<HTMLElement>element.querySelector(".unit-speed")).innerText = String(Math.floor(msToKnots(this.#speed))) + "GS";
/* Rotate elements according to heading */
element.querySelectorAll("[data-rotate-to-heading]").forEach((el) => {
const headingDeg = rad2deg(this.#track);
let currentStyle = el.getAttribute("style") || "";
el.setAttribute("style", currentStyle + `transform:rotate(${headingDeg}deg);`);
});
/* Turn on ammo indicators */
var hasFox1 = element.querySelector(".unit")?.hasAttribute("data-has-fox-1");
var hasFox2 = element.querySelector(".unit")?.hasAttribute("data-has-fox-2");
var hasFox3 = element.querySelector(".unit")?.hasAttribute("data-has-fox-3");
var hasOtherAmmo = element.querySelector(".unit")?.hasAttribute("data-has-other-ammo");
var newHasFox1 = false;
var newHasFox2 = false;
var newHasFox3 = false;
var newHasOtherAmmo = false;
Object.values(this.#ammo).forEach((ammo: Ammo) => {
if (ammo.category == 1 && ammo.missileCategory == 1) {
if (ammo.guidance == 4 || ammo.guidance == 5) newHasFox1 = true;
else if (ammo.guidance == 2) newHasFox2 = true;
else if (ammo.guidance == 3) newHasFox3 = true;
} else newHasOtherAmmo = true;
});
if (hasFox1 != newHasFox1) element.querySelector(".unit")?.toggleAttribute("data-has-fox-1", newHasFox1);
if (hasFox2 != newHasFox2) element.querySelector(".unit")?.toggleAttribute("data-has-fox-2", newHasFox2);
if (hasFox3 != newHasFox3) element.querySelector(".unit")?.toggleAttribute("data-has-fox-3", newHasFox3);
if (hasOtherAmmo != newHasOtherAmmo) element.querySelector(".unit")?.toggleAttribute("data-has-other-ammo", newHasOtherAmmo);
/* Draw the hotgroup element */
element.querySelector(".unit")?.toggleAttribute("data-is-in-hotgroup", this.#hotgroup != null);
if (this.#hotgroup) {
const hotgroupEl = element.querySelector(".unit-hotgroup-id") as HTMLElement;
if (hotgroupEl) hotgroupEl.innerText = String(this.#hotgroup);
}
/* Set bullseyes positions */
const bullseyes = getApp().getMissionManager().getBullseyes();
if (Object.keys(bullseyes).length > 0) {
const bullseye = `${computeBearingRangeString(bullseyes[coalitionToEnum(getApp().getMap().getOptions().AWACSCoalition)].getLatLng(), this.getPosition())}`;
if (element.querySelector(".unit-bullseye")) (<HTMLElement>element.querySelector(".unit-bullseye")).innerText = `${bullseye}`;
}
/* Set BRAA */
const reference = getApp().getUnitsManager().getAWACSReference();
if (reference && reference !== this && reference.getAlive()) {
const BRAA = `${computeBearingRangeString(reference.getPosition(), this.getPosition())}`;
if (element.querySelector(".unit-braa")) (<HTMLElement>element.querySelector(".unit-braa")).innerText = `${BRAA}`;
} else if (element.querySelector(".unit-braa")) (<HTMLElement>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"
);
}
/* Set vertical offset for altitude stacking */
var pos = getApp().getMap().latLngToLayerPoint(this.getLatLng()).round();
this.setZIndexOffset(1000 + Math.floor(this.#position.alt as number) - pos.y + (this.#highlighted || this.#selected ? 5000 : 0));
/* Get the cluster this unit is in to position the label correctly */
let cluster = Object.values(getApp().getUnitsManager().getClusters()).find((cluster) => cluster.includes(this));
if (cluster && cluster.length > 1) {
let clusterMean = turf.centroid(turf.featureCollection(cluster.map((unit) => turf.point([unit.getPosition().lng, unit.getPosition().lat]))));
let bearingFromCluster = bearing(
clusterMean.geometry.coordinates[1],
clusterMean.geometry.coordinates[0],
this.getPosition().lat,
this.getPosition().lng,
false
);
if (bearingFromCluster < 0) bearingFromCluster += 360;
let trackIndex = Math.round(bearingFromCluster / 45);
for (let idx = 0; idx < bearingStrings.length; idx++) element?.querySelector(".unit-summary")?.classList.remove("cluster-" + bearingStrings[idx]);
element?.querySelector(".unit-summary")?.classList.add("cluster-" + bearingStrings[trackIndex]);
} else {
for (let idx = 0; idx < bearingStrings.length; idx++) element?.querySelector(".unit-summary")?.classList.remove("cluster-" + bearingStrings[idx]);
element?.querySelector(".unit-summary")?.classList.add("cluster-north-east");
}
/* Draw the contact trail */
if (/*TODO getApp().getMap().getOptions().AWACSMode*/ false) {
this.#trailPolylines = this.#trailPositions.map(
(latlng, idx) => new Polyline([latlng, latlng], { color: colors.WHITE, opacity: 1 - (idx + 1) / TRAIL_LENGTH })
);
this.#trailPolylines.forEach((polyline) => polyline.addTo(getApp().getMap()));
}
}

View File

@ -241,6 +241,7 @@ export class UnitsManager {
if (datumIndex == DataIndexes.category) {
const category = dataExtractor.extractString();
this.addUnit(ID, category);
} else {
/* Inconsistent data, we need to wait for a refresh */
return updateTime;
@ -250,6 +251,7 @@ export class UnitsManager {
if (ID in this.#units) {
this.#units[ID].setData(dataExtractor);
this.#units[ID].getAlive() && updatedUnits.push(this.#units[ID]);
}
}

View File

@ -1072,10 +1072,69 @@ function Olympus.setOnOff(groupName, onOff)
end
end
-- Disable the AI of a group on or off entirely
function Olympus.setRedGreenAuto(groupName, redGreenAuto)
Olympus.debug("Olympus.setRedGreenAuto " .. groupName .. " " .. tostring(redGreenAuto), 2)
local group = Group.getByName(groupName)
if group ~= nil then
if redGreenAuto == 'RED' then
group:getController():setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED)
end
if redGreenAuto == 'GREEN' then
group:getController():setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.GREEN)
end
if redGreenAuto == 'AUTO' then
group:getController():setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO)
end
Olympus.debug("Olympus.setRedGreenAuto completed successfully", 2)
end
end
function getUnitDescription(unit)
return unit:getDescr()
end
-- This function gets the navpoints from the DCS mission
function Olympus.getNavPoints()
local function extract_tag(str)
return str:match("^%[(.-)%]")
end
local navpoints = {}
if mist.DBs.navPoints ~= nil then
for coalitionName, coalitionNavpoints in pairs(mist.DBs.navPoints) do
if navpoints[coalitionName] == nil then
navpoints[coalitionName] = {}
end
for index, navpointDrawingData in pairs(coalitionNavpoints) do
local navpointCustomLayer = extract_tag(navpointDrawingData['callsignStr']);
-- Let's convert DCS coords to lat lon
local vec3 = { x = navpointDrawingData['x'], y = 0, z = navpointDrawingData['y'] }
local lat, lng = coord.LOtoLL(vec3)
navpointDrawingData['lat'] = lat
navpointDrawingData['lng'] = lng
navpointDrawingData['coalition'] = coalitionName
if navpointCustomLayer ~= nil then
if navpoints[coalitionName][navpointCustomLayer] == nil then
navpoints[coalitionName][navpointCustomLayer] = {}
end
navpoints[coalitionName][navpointCustomLayer][navpointDrawingData['callsignStr']] = navpointDrawingData
else
navpoints[coalitionName][navpointDrawingData['callsignStr']] = navpointDrawingData
end
end
end
end
return navpoints
end
-- This function is periodically called to collect the data of all the existing drawings in the mission to be transmitted to the olympus.dll
function Olympus.initializeDrawings()
local drawings = {}
@ -1129,6 +1188,10 @@ function Olympus.initializeDrawings()
end
end
local navpoints = Olympus.getNavPoints()
drawings['navpoints'] = navpoints
Olympus.drawingsByLayer["drawings"] = drawings
-- Send the drawings to the DLL
@ -1214,6 +1277,18 @@ function Olympus.setUnitsData(arg, time)
end
table["isAlive"] = unit:isExist() and unit:isActive() and unit:getLife() >= 1
table["radarState"] = "AUTO"
if unit:isActive() then
if unit:getRadar() then
-- Olympus.log:info("radarState: unit active and getRadar is true, setting RED.")
table["radarState"] = "RED"
else
-- Olympus.log:info("radarState: unit active and getRadar is false, setting GREEN.")
table["radarState"] = "GREEN"
end
end
local group = unit:getGroup()
if group ~= nil then