Merge branch 'release-candidate' of https://github.com/Pax1601/DCSOlympus into release-candidate

This commit is contained in:
Pax1601
2025-03-24 23:03:40 +01:00
22 changed files with 1175 additions and 4082 deletions

View File

@@ -451,6 +451,8 @@ export enum DataIndexes {
startOfData = 0,
category,
alive,
alarmState,
radarState,
human,
controlled,
coalition,

View File

@@ -173,6 +173,7 @@ export interface ObjectIconOptions {
showCallsign: boolean;
rotateToHeading: boolean;
showCluster: boolean;
showAlarmState: boolean;
}
export interface GeneralSettings {
@@ -220,6 +221,7 @@ export interface UnitData {
markerCategory: string;
ID: number;
alive: boolean;
alarmState: AlarmState;
human: boolean;
controlled: boolean;
coalition: string;
@@ -414,3 +416,9 @@ export interface Drawing {
file?: string;
scale?: number;
}
export enum AlarmState {
RED = 'red',
GREEN = 'green',
AUTO = 'auto'
}

View File

@@ -659,6 +659,33 @@
display: inline;
}
/* Unit Radar State */
.unit-alarm-state {
background-repeat: no-repeat;
height: 16px;
width: 16px;
position: absolute;
border-radius: 50%;
border: none;
background: transparent;
left: 35px;
bottom: 0px;
}
.unit[data-alarm-state="green"] .unit-alarm-state {
background-image: url("/images/states/alarm-state-green.svg");
}
.unit[data-alarm-state="red"] .unit-alarm-state {
background-image: url("/images/states/alarm-state-red.svg");
}
.unit[data-alarm-state="auto"] .unit-alarm-state {
border: 0px solid transpare;
background: transparent;
}
[todo-data-awacs-mode] [data-object|="unit"] .unit-selected-spotlight,
[todo-data-awacs-mode] [data-object|="unit"] .unit-short-label,
[todo-data-awacs-mode] [data-object|="unit"] .unit-state,

View File

@@ -1,12 +1,12 @@
import { Circle, LatLng, Polygon } from "leaflet";
import * as turf from "@turf/turf";
import { ROEs, emissionsCountermeasures, reactionsToThreat, states } from "../constants/constants";
import { DateAndTime } from "../interfaces";
import { AlarmState, DateAndTime } from "../interfaces";
import { Converter } from "usng";
import { MGRS } from "../types/types";
import { featureCollection } from "turf";
import MagVar from "magvar";
import axios from 'axios';
import axios from "axios";
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number, magnetic = true) {
const φ1 = deg2rad(lat1); // φ, λ in radians
@@ -58,19 +58,19 @@ export function midpoint(lat1: number, lon1: number, lat2: number, lon2: number,
const λ2 = deg2rad(lon2); // Convert longitude of point 2 from degrees to radians
// Convert point 1 to Mercator projection coordinates
const x1 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI + λ1);
const y1 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI - Math.log(Math.tan(Math.PI / 4 + φ1 / 2)));
const x1 = (1 / (2 * Math.PI)) * Math.pow(2, zoom) * (Math.PI + λ1);
const y1 = (1 / (2 * Math.PI)) * Math.pow(2, zoom) * (Math.PI - Math.log(Math.tan(Math.PI / 4 + φ1 / 2)));
// Convert point 2 to Mercator projection coordinates
const x2 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI + λ2);
const y2 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI - Math.log(Math.tan(Math.PI / 4 + φ2 / 2)));
const x2 = (1 / (2 * Math.PI)) * Math.pow(2, zoom) * (Math.PI + λ2);
const y2 = (1 / (2 * Math.PI)) * Math.pow(2, zoom) * (Math.PI - Math.log(Math.tan(Math.PI / 4 + φ2 / 2)));
// Calculate the midpoint in Mercator projection coordinates
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
// Convert the midpoint back to latitude and longitude
const λ = (2 * Math.PI * mx / Math.pow(2, zoom)) - Math.PI;
const λ = (2 * Math.PI * mx) / Math.pow(2, zoom) - Math.PI;
const φ = 2 * Math.atan(Math.exp(Math.PI - (2 * Math.PI * my) / Math.pow(2, zoom))) - Math.PI / 2;
// Return the midpoint as a LatLng object
@@ -286,6 +286,19 @@ export function enumToROE(ROE: number) {
else return ROEs[0];
}
export function enumToAlarmState(alarmState: number) {
switch (alarmState) {
case 2:
return AlarmState.RED;
case 1:
return AlarmState.GREEN;
case 0:
return AlarmState.AUTO;
default:
return AlarmState.AUTO;
}
}
export function convertROE(idx: number) {
let roe = 0;
if (idx === 0) roe = 4;
@@ -522,7 +535,7 @@ export function computeStandardFormationOffset(formation, idx) {
var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4);
var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4);
offset = { x: xl * 50, y: yl * 50, z: 0 };
offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1
offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1;
if (yr == 0) {
layer++;
xr = 0;
@@ -558,7 +571,7 @@ export function roundToNearestFive(number) {
return Math.round(number / 5) * 5;
}
export function toDCSFormationOffset(offset: {x: number, y: number, z: number}) {
export function toDCSFormationOffset(offset: { x: number; y: number; z: number }) {
// X: front-rear, positive front
// Y: top-bottom, positive top
// Z: left-right, positive right
@@ -566,7 +579,7 @@ export function toDCSFormationOffset(offset: {x: number, y: number, z: number})
return { x: -offset.y, y: offset.z, z: offset.x };
}
export function fromDCSFormationOffset(offset: {x: number, y: number, z: number}) {
export function fromDCSFormationOffset(offset: { x: number; y: number; z: number }) {
return { x: offset.z, y: -offset.x, z: offset.y };
}
@@ -579,7 +592,7 @@ export function fromDCSFormationOffset(offset: {x: number, y: number, z: number}
export function adjustBrightness(color, percent) {
// Ensure the color is in the correct format
if (!/^#[0-9A-F]{6}$/i.test(color)) {
throw new Error('Invalid color format. Use #RRGGBB.');
throw new Error("Invalid color format. Use #RRGGBB.");
}
// Parse the color components
@@ -605,12 +618,12 @@ export function adjustBrightness(color, percent) {
export function setOpacity(color, opacity) {
// Ensure the color is in the correct format
if (!/^#[0-9A-F]{6}$/i.test(color)) {
throw new Error('Invalid color format. Use #RRGGBB.');
throw new Error("Invalid color format. Use #RRGGBB.");
}
// Ensure the opacity is within the valid range
if (opacity < 0 || opacity > 1) {
throw new Error('Opacity must be between 0 and 1.');
throw new Error("Opacity must be between 0 and 1.");
}
// Parse the color components
@@ -630,7 +643,7 @@ export function setOpacity(color, opacity) {
export function computeBrightness(color) {
// Ensure the color is in the correct format
if (!/^#[0-9A-F]{6}$/i.test(color)) {
throw new Error('Invalid color format. Use #RRGGBB.');
throw new Error("Invalid color format. Use #RRGGBB.");
}
// Parse the color components
@@ -660,10 +673,10 @@ export function normalizeAngle(angle: number): number {
}
export function decimalToRGBA(decimal: number): string {
const r = (decimal >>> 24) & 0xff;
const g = (decimal >>> 16) & 0xff;
const b = (decimal >>> 8) & 0xff;
const a = (decimal & 0xff) / 255;
const r = (decimal >>> 24) & 0xff;
const g = (decimal >>> 16) & 0xff;
const b = (decimal >>> 8) & 0xff;
const a = (decimal & 0xff) / 255;
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
}
@@ -671,18 +684,18 @@ export function decimalToRGBA(decimal: number): string {
export async function getWikipediaImage(unitName: string): Promise<string | null> {
try {
// Search for the unit name on Wikipedia
const searchResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
const searchResponse = await axios.get("https://en.wikipedia.org/w/api.php", {
params: {
action: 'query',
list: 'search',
action: "query",
list: "search",
srsearch: unitName,
format: 'json',
origin: '*'
}
format: "json",
origin: "*",
},
});
if (searchResponse.data.query.search.length === 0) {
console.error('No search results found for the unit name.');
console.error("No search results found for the unit name.");
return null;
}
@@ -690,15 +703,15 @@ export async function getWikipediaImage(unitName: string): Promise<string | null
const pageTitle = searchResponse.data.query.search[0].title;
// Get the page content to find the image
const pageResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
const pageResponse = await axios.get("https://en.wikipedia.org/w/api.php", {
params: {
action: 'query',
action: "query",
titles: pageTitle,
prop: 'pageimages',
prop: "pageimages",
pithumbsize: 500,
format: 'json',
origin: '*'
}
format: "json",
origin: "*",
},
});
const pages = pageResponse.data.query.pages;
@@ -708,11 +721,11 @@ export async function getWikipediaImage(unitName: string): Promise<string | null
if (page.thumbnail && page.thumbnail.source) {
return page.thumbnail.source;
} else {
console.error('No image found for the unit name.');
console.error("No image found for the unit name.");
return null;
}
} catch (error) {
console.error('Error fetching data from Wikipedia:', error);
console.error("Error fetching data from Wikipedia:", error);
return null;
}
}
@@ -720,18 +733,18 @@ export async function getWikipediaImage(unitName: string): Promise<string | null
export async function getWikipediaSummary(unitName: string): Promise<string | null> {
try {
// Search for the unit name on Wikipedia
const searchResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
const searchResponse = await axios.get("https://en.wikipedia.org/w/api.php", {
params: {
action: 'query',
list: 'search',
action: "query",
list: "search",
srsearch: unitName,
format: 'json',
origin: '*'
}
format: "json",
origin: "*",
},
});
if (searchResponse.data.query.search.length === 0) {
console.error('No search results found for the unit name.');
console.error("No search results found for the unit name.");
return null;
}
@@ -739,16 +752,16 @@ export async function getWikipediaSummary(unitName: string): Promise<string | nu
const pageTitle = searchResponse.data.query.search[0].title;
// Get the page content to find the summary
const pageResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
const pageResponse = await axios.get("https://en.wikipedia.org/w/api.php", {
params: {
action: 'query',
prop: 'extracts',
action: "query",
prop: "extracts",
exintro: true,
explaintext: true,
titles: pageTitle,
format: 'json',
origin: '*'
}
format: "json",
origin: "*",
},
});
const pages = pageResponse.data.query.pages;
@@ -758,11 +771,11 @@ export async function getWikipediaSummary(unitName: string): Promise<string | nu
if (page.extract) {
return page.extract;
} else {
console.error('No summary found for the unit name.');
console.error("No summary found for the unit name.");
return null;
}
} catch (error) {
console.error('Error fetching data from Wikipedia:', error);
console.error("Error fetching data from Wikipedia:", error);
return null;
}
}
@@ -777,4 +790,4 @@ export function secondsToTimeString(seconds: number) {
export function isTrustedEnvironment() {
return window.location.protocol === "https:";
}
}

View File

@@ -17,6 +17,7 @@ import {
} from "../constants/constants";
import {
AirbasesData,
AlarmState,
BullseyesData,
CommandModeOptions,
GeneralSettings,
@@ -419,6 +420,12 @@ export class ServerManager {
this.PUT(data, callback);
}
setAlarmState(ID: number, alarmState: number, callback: CallableFunction = () => {}) {
var command = { ID: ID, alarmState: alarmState };
var data = { setAlarmState: command };
this.PUT(data, callback);
}
setReactionToThreat(ID: number, reactionToThreat: string, callback: CallableFunction = () => {}) {
var command = {
ID: ID,

File diff suppressed because one or more lines are too long

View File

@@ -19,6 +19,9 @@ import {
import { OlToggle } from "../components/oltoggle";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
import {
olButtonsAlarmstateAuto,
olButtonsAlarmstateGreen,
olButtonsAlarmstateRed,
olButtonsEmissionsAttack,
olButtonsEmissionsDefend,
olButtonsEmissionsFree,
@@ -54,7 +57,7 @@ import { OlSearchBar } from "../components/olsearchbar";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
import { FaRadio, FaVolumeHigh } from "react-icons/fa6";
import { OlNumberInput } from "../components/olnumberinput";
import { GeneralSettings, Radio, TACAN } from "../../interfaces";
import { AlarmState, GeneralSettings, Radio, TACAN } from "../../interfaces";
import { OlStringInput } from "../components/olstringinput";
import { OlFrequencyInput } from "../components/olfrequencyinput";
import { UnitSink } from "../../audio/unitsink";
@@ -87,6 +90,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
radio: undefined as undefined | Radio,
TACAN: undefined as undefined | TACAN,
generalSettings: undefined as undefined | GeneralSettings,
alarmState: undefined as undefined | AlarmState
};
}
@@ -130,7 +134,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
const [activeRadioSettings, setActiveRadioSettings] = useState(null as null | { radio: Radio; TACAN: TACAN });
const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | GeneralSettings);
const [lastUpdateTime, setLastUpdateTime] = useState(0);
const [showScenicModes, setShowScenicModes] = useState(true);
const [showScenicModes, setShowScenicModes] = useState(false);
const [showEngagementSettings, setShowEngagementSettings] = useState(false);
const [barrelHeight, setBarrelHeight] = useState(0);
const [muzzleVelocity, setMuzzleVelocity] = useState(0);
@@ -178,6 +182,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
onOff: (unit: Unit) => unit.getOnOff(),
radio: (unit: Unit) => unit.getRadio(),
TACAN: (unit: Unit) => unit.getTACAN(),
alarmState: (unit: Unit) => unit.getAlarmState(),
generalSettings: (unit: Unit) => unit.getGeneralSettings(),
isAudioSink: (unit: Unit) => {
return (
@@ -885,6 +890,82 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
)}
{/* ============== Rules of Engagement END ============== */}
{/* ============== Alarm state selector START ============== */}
{
<div className="flex flex-col gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Alarm State
</span>
<OlButtonGroup
tooltip={() => (
<OlExpandingTooltip
title="Alarm State"
content={
<div className="flex flex-col gap-2">
<div>Sets the alarm state of the unit, in order:</div>
<div className="flex flex-col gap-2 px-2">
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsAlarmstateGreen} className={`
my-auto min-w-8 text-white
`} /> Green: The unit will not engage with its sensors in any circumstances. The unit will be able to move.
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsAlarmstateAuto} className={`
my-auto min-w-8 text-white
`} />{" "}
<div>
{" "}
Auto: The unit will use its sensors to engage based on its ROE.
</div>
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsAlarmstateRed} className={`
my-auto min-w-8 text-white
`} /> Red: The unit will be actively searching for target with its sensors. For some units, this will deploy
the radar and make the unit not able to move.
</div>
</div>
</div>
}
/>
)}
tooltipRelativeToParent={true}
>
{[olButtonsAlarmstateGreen, olButtonsAlarmstateAuto, olButtonsAlarmstateRed].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
getApp()
.getUnitsManager()
.setAlarmState([1, 0, 2][idx], null, () =>
setForcedUnitsData({
...forcedUnitsData,
alarmState: [AlarmState.GREEN, AlarmState.AUTO, AlarmState.RED][idx],
})
);
}}
active={selectedUnitsData.alarmState === [AlarmState.GREEN, AlarmState.AUTO, AlarmState.RED][idx]}
icon={icon}
/>
);
})}
</OlButtonGroup>
</div>
}
{/* ============== Alarm state selector END ============== */}
{selectedCategories.every((category) => {
return ["Aircraft", "Helicopter"].includes(category);
}) && (
@@ -1340,7 +1421,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{/* ============== Miss on purpose toggle END ============== */}
<div className="flex gap-4">
{/* ============== Shots scatter START ============== */}
<div className={`flex flex-col gap-2`}>
<div className={`flex w-full justify-between gap-2`}>
<span
className={`
my-auto font-normal
@@ -1373,7 +1454,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
{/* ============== Shots scatter END ============== */}
{/* ============== Shots intensity START ============== */}
<div className="flex flex-col gap-2">
{/*<div className="flex flex-col gap-2">
<span
className={`
my-auto font-normal
@@ -1405,12 +1486,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</OlButtonGroup>
</div>
{/* ============== Shots intensity END ============== */}
<OlStateButton
{/*<OlStateButton
className="mt-auto"
checked={showEngagementSettings}
onClick={() => setShowEngagementSettings(!showEngagementSettings)}
icon={faCog}
></OlStateButton>
*/}
</div>
{/* ============== Operate as toggle START ============== */}
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (

View File

@@ -18,6 +18,7 @@ import {
computeBearingRangeString,
adjustBrightness,
bearingAndDistanceToLatLng,
enumToAlarmState,
} from "../other/utils";
import { CustomMarker } from "../map/markers/custommarker";
import { SVGInjector } from "@tanem/svg-injector";
@@ -52,7 +53,7 @@ import {
} from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { Weapon } from "../weapon/weapon";
import { Ammo, Contact, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitBlueprint, UnitData } from "../interfaces";
import { AlarmState, Ammo, Contact, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitBlueprint, UnitData } from "../interfaces";
import { RangeCircle } from "../map/rangecircle";
import { Group } from "./group";
import { ContextActionSet } from "./contextactionset";
@@ -85,6 +86,8 @@ export abstract class Unit extends CustomMarker {
/* Data controlled directly by the backend. No setters are provided to avoid misalignments */
#alive: boolean = false;
#alarmState: AlarmState = AlarmState.AUTO;
#radarState: boolean | undefined = undefined;
#human: boolean = false;
#controlled: boolean = false;
#coalition: string = "neutral";
@@ -349,6 +352,12 @@ export abstract class Unit extends CustomMarker {
getRaceTrackBearing() {
return this.#racetrackBearing;
}
getAlarmState() {
return this.#alarmState;
}
getRadarState() {
return this.#radarState;
}
getTimeToNextTasking() {
return this.#timeToNextTasking;
}
@@ -538,6 +547,7 @@ export abstract class Unit extends CustomMarker {
var datumIndex = 0;
while (datumIndex != DataIndexes.endOfData) {
datumIndex = dataExtractor.extractUInt8();
switch (datumIndex) {
case DataIndexes.category:
dataExtractor.extractString();
@@ -546,6 +556,10 @@ export abstract class Unit extends CustomMarker {
this.setAlive(dataExtractor.extractBool());
updateMarker = true;
break;
case DataIndexes.radarState:
this.#radarState = dataExtractor.extractBool();
updateMarker = true;
break;
case DataIndexes.human:
this.#human = dataExtractor.extractBool();
break;
@@ -651,6 +665,9 @@ export abstract class Unit extends CustomMarker {
case DataIndexes.ROE:
this.#ROE = enumToROE(dataExtractor.extractUInt8());
break;
case DataIndexes.alarmState:
this.#alarmState = enumToAlarmState(dataExtractor.extractUInt8());
break;
case DataIndexes.reactionToThreat:
this.#reactionToThreat = enumToReactionToThreat(dataExtractor.extractUInt8());
break;
@@ -845,6 +862,7 @@ export abstract class Unit extends CustomMarker {
racetrackLength: this.#racetrackLength,
racetrackAnchor: this.#racetrackAnchor,
racetrackBearing: this.#racetrackBearing,
alarmState: this.#alarmState,
timeToNextTasking: this.#timeToNextTasking,
barrelHeight: this.#barrelHeight,
muzzleVelocity: this.#muzzleVelocity,
@@ -870,6 +888,13 @@ export abstract class Unit extends CustomMarker {
}
}
setRadarState(newRadarState: boolean) {
if (newRadarState != this.#radarState) {
this.#radarState = newRadarState;
this.#updateMarker();
}
}
/** Set the unit as user-selected
*
* @param selected (boolean)
@@ -1166,6 +1191,13 @@ export abstract class Unit extends CustomMarker {
el.append(healthIndicator);
}
/* Alarm state indicator */
if (iconOptions.showAlarmState) {
var alarmStateIcon = document.createElement("div");
alarmStateIcon.classList.add("unit-alarm-state");
el.append(alarmStateIcon);
}
/* Ammo indicator */
if (iconOptions.showAmmo) {
var ammoIndicator = document.createElement("div");
@@ -1401,6 +1433,10 @@ export abstract class Unit extends CustomMarker {
if (!this.#human) getApp().getServerManager().setROE(this.ID, ROE);
}
setAlarmState(alarmState: number) {
if (!this.#human) getApp().getServerManager().setAlarmState(this.ID, alarmState);
}
setReactionToThreat(reactionToThreat: string) {
if (!this.#human) getApp().getServerManager().setReactionToThreat(this.ID, reactionToThreat);
}
@@ -1725,165 +1761,168 @@ 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 radar state*/
element.querySelector(".unit")?.setAttribute("data-alarm-state", this.#alarmState);
/* 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);
}
/* Draw the cluster element */
element
.querySelector(".unit")
?.toggleAttribute(
"data-is-cluster-leader",
this.#isClusterLeader &&
this.#clusterUnits.length > 1 &&
getApp().getMap().getOptions().clusterGroundUnits &&
getApp().getMap().getZoom() < CLUSTERING_ZOOM_TRANSITION &&
!this.getSelected()
);
if (this.#isClusterLeader && this.#clusterUnits.length > 1) {
const clusterEl = element.querySelector(".unit-cluster-id") as HTMLElement;
if (clusterEl) clusterEl.innerText = String(this.#clusterUnits.length);
}
/* 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);
}
/* Draw the cluster element */
element
.querySelector(".unit")
?.toggleAttribute(
"data-is-cluster-leader",
this.#isClusterLeader &&
this.#clusterUnits.length > 1 &&
getApp().getMap().getOptions().clusterGroundUnits &&
getApp().getMap().getZoom() < CLUSTERING_ZOOM_TRANSITION &&
!this.getSelected()
);
if (this.#isClusterLeader && this.#clusterUnits.length > 1) {
const clusterEl = element.querySelector(".unit-cluster-id") as HTMLElement;
if (clusterEl) clusterEl.innerText = String(this.#clusterUnits.length);
}
/* 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()));
}
}
@@ -2394,6 +2433,7 @@ export abstract class AirUnit extends Unit {
showCallsign: belongsToCommandedCoalition && /*TODO !getApp().getMap().getOptions().AWACSMode || */ this.getHuman(),
rotateToHeading: false,
showCluster: false,
showAlarmState: false
} as ObjectIconOptions;
}
@@ -2482,6 +2522,7 @@ export class GroundUnit extends Unit {
showCallsign: belongsToCommandedCoalition && /*TODO !getApp().getMap().getOptions().AWACSMode || */ this.getHuman(),
rotateToHeading: false,
showCluster: true,
showAlarmState: true,
} as ObjectIconOptions;
}
@@ -2549,6 +2590,7 @@ export class NavyUnit extends Unit {
showCallsign: belongsToCommandedCoalition && /*TODO !getApp().getMap().getOptions().AWACSMode || */ this.getHuman(),
rotateToHeading: false,
showCluster: false,
showAlarmState: true
} as ObjectIconOptions;
}

View File

@@ -1,36 +1,14 @@
import { DomEvent, DomUtil, LatLng, LatLngBounds } from "leaflet";
import { getApp } from "../olympusapp";
import { AirUnit, GroundUnit, NavyUnit, Unit } from "./unit";
import {
areaContains,
bearingAndDistanceToLatLng,
deepCopyTable,
deg2rad,
getGroundElevation,
latLngToMercator,
mToFt,
mercatorToLatLng,
msToKnots,
} from "../other/utils";
import { CoalitionPolygon } from "../map/coalitionarea/coalitionpolygon";
import * as turf from "@turf/turf";
import { DomEvent, LatLng, LatLngBounds } from "leaflet";
import {
BLUE_COMMANDER,
DELETE_CYCLE_TIME,
DELETE_SLOW_THRESHOLD,
DataIndexes,
GAME_MASTER,
IADSDensities,
OlympusState,
RED_COMMANDER,
UnitControlSubState,
UnitControlSubState
} from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { citiesDatabase } from "./databases/citiesdatabase";
import { TemporaryUnitMarker } from "../map/markers/temporaryunitmarker";
import { Contact, GeneralSettings, Radio, TACAN, UnitBlueprint, UnitData, UnitSpawnTable } from "../interfaces";
import { Group } from "./group";
import { CoalitionCircle } from "../map/coalitionarea/coalitioncircle";
import { ContextActionSet } from "./contextactionset";
import {
AWACSReferenceChangedEvent,
CommandModeOptionsChangedEvent,
@@ -46,10 +24,31 @@ import {
UnitsRefreshedEvent,
UnitsUpdatedEvent,
} from "../events";
import { UnitDatabase } from "./databases/unitdatabase";
import * as turf from "@turf/turf";
import { Contact, GeneralSettings, Radio, TACAN, UnitBlueprint, UnitData, UnitSpawnTable } from "../interfaces";
import { CoalitionCircle } from "../map/coalitionarea/coalitioncircle";
import { CoalitionPolygon } from "../map/coalitionarea/coalitionpolygon";
import { PathMarker } from "../map/markers/pathmarker";
import { TemporaryUnitMarker } from "../map/markers/temporaryunitmarker";
import { getApp } from "../olympusapp";
import {
areaContains,
bearingAndDistanceToLatLng,
deepCopyTable,
deg2rad,
getGroundElevation,
latLngToMercator,
mToFt,
mercatorToLatLng,
msToKnots,
} from "../other/utils";
import { DataExtractor } from "../server/dataextractor";
import { Coalition } from "../types/types";
import { ContextActionSet } from "./contextactionset";
import { citiesDatabase } from "./databases/citiesdatabase";
import { UnitDatabase } from "./databases/unitdatabase";
import { Group } from "./group";
import { AirUnit, GroundUnit, NavyUnit, Unit } from "./unit";
/** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only
* result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows
@@ -252,6 +251,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;
@@ -261,6 +261,7 @@ export class UnitsManager {
if (ID in this.#units) {
this.#units[ID].setData(dataExtractor);
this.#units[ID].getAlive() && updatedUnits.push(this.#units[ID]);
}
}
@@ -779,6 +780,27 @@ export class UnitsManager {
this.#protectionCallback = callback;
} else callback(units);
}
/** Set a specific Alarm State to all the selected units
*
* @param AlarmState Value to set, see constants for acceptable values
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
setAlarmState(alarmState: number, units: Unit[] | null = null, onExecution: () => void = () => {}) {
if (units === null) units = this.getSelectedUnits();
units = units.filter((unit) => !unit.getHuman());
let callback = (units) => {
onExecution();
units.forEach((unit: Unit) => unit.setAlarmState(alarmState));
this.#showActionMessage(units, `Alarm State set to ${alarmState.toString()}`);
};
if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION);
this.#protectionCallback = callback;
} else callback(units);
}
/** Set a specific reaction to threat to all the selected units
*
* @param reactionToThreat Value to set, see constants for acceptable values