More work on AWACS mode

This commit is contained in:
Davide Passoni 2024-11-27 17:36:32 +01:00
parent a92d4403d7
commit b4841872ca
12 changed files with 286 additions and 188 deletions

View File

@ -36,6 +36,7 @@
},
"devDependencies": {
"@eslint/js": "^9.6.0",
"@turf/clusters": "^7.1.0",
"@types/node": "^22.5.1",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",

View File

@ -1,5 +1,5 @@
import { LatLng, LatLngBounds } from "leaflet";
import { MapOptions } from "../types/types";
import { Coalition, MapOptions } from "../types/types";
import { CommandModeOptions } from "../interfaces";
import { ContextAction } from "../unit/contextaction";
import {
@ -30,6 +30,8 @@ export const SELECT_TOLERANCE_PX = 5;
export const SHORT_PRESS_MILLISECONDS = 200;
export const DEBOUNCE_MILLISECONDS = 200;
export const TRAIL_LENGTH = 10;
export const UNITS_URI = "units";
export const WEAPONS_URI = "weapons";
export const LOGS_URI = "logs";
@ -349,9 +351,8 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
cameraPluginEnabled: false,
cameraPluginMode: "map",
tabletMode: false,
showUnitBullseyes: false,
showUnitBRAA: false,
AWACSMode: false
AWACSMode: false,
AWACSCoalition: "blue"
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {

View File

@ -130,8 +130,6 @@ export interface ObjectIconOptions {
showSummary: boolean;
showCallsign: boolean;
rotateToHeading: boolean;
showBullseyes: boolean;
showBRAA: boolean;
}
export interface GeneralSettings {

View File

@ -228,8 +228,6 @@ export class Map extends L.Map {
MapOptionsChangedEvent.on((options: MapOptions) => {
this.getContainer().toggleAttribute("data-awacs-mode", options.AWACSMode);
this.getContainer().toggleAttribute("data-hide-labels", !options.showUnitLabels);
this.getContainer().toggleAttribute("data-hide-bullseyes", !options.showUnitBullseyes);
this.getContainer().toggleAttribute("data-hide-BRAA", !options.showUnitBRAA);
this.#cameraControlPort = options.cameraPluginPort;
this.#cameraZoomRatio = 50 / (20 + options.cameraPluginRatio);
this.#slaveDCSCamera = options.cameraPluginEnabled;
@ -242,6 +240,8 @@ export class Map extends L.Map {
}, 500); // DCS does not always apply the altitude correctly at the first set when changing map type
}
if (options.AWACSMode && this.#layerName !== "AWACS") this.setLayerName("AWACS");
this.updateMinimap();
});

View File

@ -1,6 +1,5 @@
/*** Unit marker elements ***/
[data-object|="unit"] {
align-items: center;
cursor: pointer;
display: flex;
height: 100%;
@ -93,17 +92,6 @@
display: none;
}
[data-awacs-mode] [data-is-selected] {
animation: blinker 0.5s linear infinite;
}
@keyframes blinker {
50% {
opacity: 30%;
}
}
/*** Basic colours ***/
[data-coalition="blue"] .unit-icon svg > *:first-child {
fill: var(--unit-background-blue);
@ -140,6 +128,22 @@
stroke: var(--unit-background-neutral) !important;
}
[data-awacs-mode] [data-is-selected] .unit-icon svg {
stroke: #FF0 !important;
}
[data-awacs-mode] [data-is-selected] .unit-vvi {
background-color: #FF0 !important;
}
[data-awacs-mode] [data-is-selected] .unit-summary {
color: #FF0 !important;
}
[data-awacs-mode] [data-is-selected] .unit-summary::after {
background-color: #FF0 !important;
}
/*** Cursors ***/
[data-is-dead],
[data-object|="unit-missile"],
@ -155,6 +159,7 @@
line-height: normal;
position: absolute;
font-size: 12px;
translate: 0px 18px;
}
[data-object|="unit-groundunit"] .unit-short-label {
@ -169,7 +174,7 @@
display: none;
height: var(--unit-health-height);
position: absolute;
translate: var(--unit-health-x) var(--unit-health-y);
translate: var(--unit-health-x) calc(25px + var(--unit-health-y));
width: var(--unit-health-width);
}
@ -181,7 +186,7 @@
display: none;
height: var(--unit-fuel-height);
position: absolute;
translate: var(--unit-fuel-x) var(--unit-fuel-y);
translate: var(--unit-fuel-x) calc(25px + var(--unit-fuel-y));
width: var(--unit-fuel-width);
}
@ -198,7 +203,7 @@
display: none;
height: fit-content;
position: absolute;
translate: var(--unit-ammo-x) var(--unit-ammo-y);
translate: var(--unit-ammo-x) calc(25px + var(--unit-ammo-y));
width: fit-content;
}
@ -217,7 +222,7 @@
flex-wrap: wrap;
font-size: 11px;
font-weight: bold;
justify-content: right;
justify-content: start;
line-height: 12px;
pointer-events: none;
position: absolute;
@ -227,24 +232,89 @@
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
right: 120%;
width: fit-content;
width: 100px;
translate: 80px 10px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary {
top: -40px;
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north {
translate: 50px -45px;
}
[data-awacs-mode] .unit-summary::before {
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north-east {
translate: 76px -32px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-east {
translate: 95px 7px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south-east {
translate: 79px 50px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south {
translate: 50px 63px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south-west {
translate: -68px 50px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-west {
translate: -80px 7px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north-west {
translate: -69px -35px;
}
[data-awacs-mode] .unit-summary::after {
content: " ";
background-color: white;
width: 40px;
height: 1px;
transform: rotate(45deg);
left: 40px;
top: 50px;
position: relative;
position: absolute;
z-index: -1;
transform-origin: 0% 0%;
top: 30px
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north::after {
transform: rotate(90deg);
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north-east::after {
transform: rotate(135deg);
translate: 2px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-east::after {
transform: rotate(180deg);
translate: -5px -12px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south-east::after {
transform: rotate(225deg);
translate: -2px -28px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south::after {
transform: rotate(270deg);
translate: -0px -28px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-south-west::after {
transform: rotate(315deg);
translate: 90px -28px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-west::after {
translate: 90px -12px;
}
[data-awacs-mode] [data-object|="unit"] .unit-summary.cluster-north-west::after {
transform: rotate(45deg);
translate: 90px;
}
[data-hide-labels] [data-object|="unit"] .unit-summary {
@ -262,10 +332,11 @@
transform-origin: right;
white-space: nowrap;
width: 80px;
text-overflow: ellipsis;
}
[data-object|="unit"]:hover .unit-summary .unit-callsign {
direction: rtl;
direction: ltr;
overflow: visible;
}
@ -470,58 +541,13 @@
opacity: 0.5;
}
/*** Unit summary ***/
[data-object|="unit"] .unit-tactical {
color: white;
column-gap: 6px;
display: flex;
flex-wrap: wrap;
font-size: 11px;
font-weight: bold;
justify-content: left;
line-height: 12px;
pointer-events: none;
position: absolute;
row-gap: 1px;
right: 120%;
top: -7px;
width: fit-content;
}
[data-object|="unit"] .unit-bullseyes {
display: flex;
line-height: 12px;
flex-wrap: wrap;
row-gap: 2px;
}
[data-hide-labels] [data-object|="unit"] .unit-tactical {
.unit-bullseye, .unit-braa {
display: none;
}
[data-hide-bullseyes] [data-object|="unit"] .unit-bullseyes {
display: none;
}
[data-hide-BRAA] [data-object|="unit"] .unit-braa {
display: none;
}
[data-object|="unit"] .unit-bullseyes {
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
}
[data-object|="unit"] .unit-braa {
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
[data-awacs-mode] .unit-bullseye,
[data-awacs-mode] .unit-braa {
display: inline;
}
[data-awacs-mode] [data-object|="unit"] .unit-selected-spotlight,

View File

@ -56,8 +56,7 @@ export class TemporaryUnitMarker extends CustomMarker {
var unitIcon = document.createElement("div");
unitIcon.classList.add("unit-icon");
var img = document.createElement("img");
img.src = `/vite/images/units/${blueprint.markerFile ?? blueprint.category}.svg`;
img.src = `/vite/images/units/map/${getApp().getMap().getOptions().AWACSMode ? "awacs" : "normal"}/${this.#coalition}/${blueprint.markerFile ?? blueprint.category}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
unitIcon.toggleAttribute("data-rotate-to-heading", false);

View File

@ -14,7 +14,7 @@ import {
reactionsToThreat,
} from "../constants/constants";
import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces";
import { ServerStatusUpdatedEvent } from "../events";
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent } from "../events";
export class ServerManager {
#connected: boolean = false;
@ -28,6 +28,7 @@ export class ServerManager {
#serverIsPaused: boolean = false;
#intervals: number[] = [];
#requests: { [key: string]: XMLHttpRequest } = {};
#updateMode = "normal"; // normal or awacs
constructor() {
this.#lastUpdateTimes[UNITS_URI] = Date.now();
@ -44,6 +45,16 @@ export class ServerManager {
},
code: "Enter"
})
MapOptionsChangedEvent.on((mapOptions) => {
if (this.#updateMode === "normal" && mapOptions.AWACSMode) {
this.#updateMode = "awacs";
this.startUpdate();
} else if (this.#updateMode === "awacs" && !mapOptions.AWACSMode) {
this.#updateMode = "normal";
this.startUpdate();
}
})
}
setUsername(newUsername: string) {
@ -566,7 +577,7 @@ export class ServerManager {
return time;
}, false);
}
}, 250)
}, this.#updateMode === "normal"? 250: 2000)
);
this.#intervals.push(
@ -577,7 +588,7 @@ export class ServerManager {
return time;
}, false);
}
}, 250)
}, this.#updateMode === "normal"? 250: 2000)
);
this.#intervals.push(
@ -590,7 +601,7 @@ export class ServerManager {
}, true);
}
},
this.getServerIsPaused() ? 500 : 5000
5000
)
);

View File

@ -26,9 +26,8 @@ export type MapOptions = {
cameraPluginEnabled: boolean;
cameraPluginMode: string;
tabletMode: boolean;
showUnitBullseyes: boolean;
showUnitBRAA: boolean;
AWACSMode: boolean;
AWACSCoalition: Coalition;
};
export type MapHiddenTypes = {

View File

@ -7,43 +7,67 @@ import {
BullseyesDataChanged,
HotgroupsChangedEvent,
MapOptionsChangedEvent,
UnitUpdatedEvent,
} from "../../events";
import { getApp } from "../../olympusapp";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
import { Coalition } from "../../types/types";
import { FaQuestionCircle } from "react-icons/fa";
import { Unit } from "../../unit/unit";
import { Bullseye } from "../../mission/bullseye";
import { coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../../other/utils";
import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../../other/utils";
const trackStrings = ["North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West"]
const trackStrings = ["North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West", "North"]
const relTrackStrings = ["hot", "flank right", "beam right", "cold", "cold", "cold", "beam left", "flank left", "hot"]
export function AWACSMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [callsign, setCallsign] = useState("Magic");
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [coalition, setCoalition] = useState("blue" as Coalition);
const [hotgroups, setHotgroups] = useState({} as { [key: number]: Unit[] });
const [referenceUnit, setReferenceUnit] = useState(null as Unit | null);
const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye });
const [refreshTime, setRefreshTime] = useState(0);
useEffect(() => {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups }));
AWACSReferenceUnitChangedEvent.on((unit) => setReferenceUnit(unit));
AWACSReferenceUnitChangedEvent.on((unit) => setReferenceUnit(unit));
BullseyesDataChanged.on((bullseyes) => setBullseyes(bullseyes));
UnitUpdatedEvent.on((unit) => setRefreshTime(Date.now()));
}, []);
const activeGroups = Object.values(hotgroups).filter((hotgroup) => {
return hotgroup.every((unit) => unit.getCoalition() !== coalition);
});
const activeGroups = Object.values(getApp()?.getUnitsManager().computeClusters((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition, 6) ?? {});
/*Object.values(hotgroups).filter((hotgroup) => {
return hotgroup.every((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition);
});*/
let readout: string[] = [];
if (bullseyes) {
if (referenceUnit) {
readout.push(`$`);
readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s": ""}`);
readout.push(
...activeGroups.map((group, idx) => {
let order = "th";
if (idx == 0) order = "st";
else if (idx == 1) order = "nd";
else if (idx == 2) order = "rd";
let trackDegs = bearing(group[0].getPosition().lat, group[0].getPosition().lng, referenceUnit.getPosition().lat, referenceUnit.getPosition().lng) - rad2deg(group[0].getTrack())
if (trackDegs < 0) trackDegs += 360
if (trackDegs > 360) trackDegs -= 360
let trackIndex = Math.round(trackDegs / 45)
let groupLine = `${activeGroups.length > 1? (idx + 1 + "" + order + " group"): "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${ (mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, ${relTrackStrings[trackIndex]}`;
if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"
else groupLine += ", hostile"
return groupLine;
})
);
} else {
readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 && "s"}`);
readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s": ""}`);
readout.push(
...activeGroups.map((group, idx) => {
let order = "th";
@ -55,15 +79,17 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
if (trackDegs < 0) trackDegs += 360
let trackIndex = Math.round(trackDegs / 45)
let groupLine = `${idx + 1}${order} group bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(coalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${ (mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`;
let groupLine = `${activeGroups.length > 1? (idx + 1 + "" + order + " group"): "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${ (mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`;
if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"
else groupLine += ", hostile"
return groupLine;
})
);
}
}
return (
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<div
@ -75,14 +101,11 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
<>
<div className="flex content-center gap-4">
<FaQuestionCircle
className={`
my-auto min-h-5 min-w-5 text-sm text-gray-500
`}
className={`my-auto min-h-5 min-w-5 text-sm text-gray-500`}
/>
<div className="flex flex-col gap-1 text-sm text-gray-500">
<p>1 Use the coalition toggle to change your coalition as AWACS.</p>
<p>2 Set enemy unit hotgroups to automatically create picture calls to read on radio for your CAP.</p>
<p>3 Optionally, set a friendly unit as reference by right clicking on it and selecting "Set AWACS reference" to create tactical calls.</p>
<p>2 Optionally, set a friendly unit as reference by right clicking on it and selecting "Set AWACS reference" to create tactical calls.</p>
</div>
</div>
<div className="flex gap-4">
@ -101,11 +124,11 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
></input>
<OlCoalitionToggle
onClick={() => {
coalition === "blue" && setCoalition("neutral");
coalition === "neutral" && setCoalition("red");
coalition === "red" && setCoalition("blue");
mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral");
mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red");
mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition","blue");
}}
coalition={coalition}
coalition={mapOptions.AWACSCoalition}
/>
</div>
<div className="flex flex-col gap-2">
@ -118,24 +141,6 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
/>{" "}
Enable AWACS map mode
</div>
<div className="flex gap-2">
<OlToggle
onClick={() => {
getApp().getMap().setOption("showUnitBullseyes", !mapOptions.showUnitBullseyes);
}}
toggled={mapOptions.showUnitBullseyes}
/>{" "}
Show units Bullseye position
</div>
<div className="flex gap-2">
<OlToggle
onClick={() => {
getApp().getMap().setOption("showUnitBRAA", !mapOptions.showUnitBRAA);
}}
toggled={mapOptions.showUnitBRAA}
/>
Show units BRAA from reference unit
</div>
</div>
<div className="mt-4 flex flex-col gap-2">
{activeGroups.length == 0 ? (

View File

@ -42,6 +42,7 @@ import {
ContextActions,
ContextActionTarget,
SHORT_PRESS_MILLISECONDS,
TRAIL_LENGTH,
} from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { Weapon } from "../weapon/weapon";
@ -62,6 +63,8 @@ import {
UnitUpdatedEvent,
} from "../events";
const bearingStrings = ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west", "north"];
var pathIcon = new Icon({
iconUrl: "/vite/images/markers/marker-icon.png",
shadowUrl: "/vite/images/markers/marker-shadow.png",
@ -157,6 +160,8 @@ export abstract class Unit extends CustomMarker {
#targetPositionPolyline: Polyline;
#hotgroup: number | null = null;
#detectionMethods: number[] = [];
#trailPositions: LatLng[] = [];
#trailPolylines: Polyline[] = [];
/* Inputs timers */
#debounceTimeout: number | null = null;
@ -466,6 +471,8 @@ export abstract class Unit extends CustomMarker {
this.#hasTask = dataExtractor.extractBool();
break;
case DataIndexes.position:
this.#trailPositions.unshift(this.#position);
this.#trailPositions.splice(TRAIL_LENGTH);
this.#position = dataExtractor.extractLatLng();
updateMarker = true;
break;
@ -902,7 +909,7 @@ export abstract class Unit extends CustomMarker {
if (this.belongsToCommandedCoalition() || this.getDetectionMethods().some((value) => [VISUAL, OPTIC].includes(value)))
marker = this.getBlueprint()?.markerFile ?? this.getDefaultMarker();
else marker = "aircraft";
img.src = `/vite/images/units/map/${getApp().getMap().getOptions().AWACSMode? 'awacs': 'normal'}/${this.getCoalition()}/${marker}.svg`;
img.src = `/vite/images/units/map/${getApp().getMap().getOptions().AWACSMode ? "awacs" : "normal"}/${this.getCoalition()}/${marker}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
@ -968,28 +975,15 @@ export abstract class Unit extends CustomMarker {
summary.appendChild(altitude);
summary.appendChild(speed);
el.appendChild(summary);
}
var tactical = document.createElement("div");
tactical.classList.add("unit-tactical");
if (iconOptions.showBullseyes) {
var bullseyes = document.createElement("div");
bullseyes.classList.add("unit-bullseyes");
var blueBullseye = document.createElement("div");
blueBullseye.classList.add("unit-blue-bullseye");
var redBullseye = document.createElement("div");
redBullseye.classList.add("unit-red-bullseye");
bullseyes.appendChild(blueBullseye);
bullseyes.appendChild(redBullseye);
tactical.appendChild(bullseyes);
}
var bullseye = document.createElement("div");
bullseye.classList.add("unit-bullseye");
summary.appendChild(bullseye);
if (iconOptions.showBRAA) {
var BRAA = document.createElement("div");
BRAA.classList.add("unit-braa");
tactical.appendChild(BRAA);
summary.appendChild(BRAA);
}
el.appendChild(tactical);
this.getElement()?.appendChild(el);
}
@ -1337,7 +1331,10 @@ export abstract class Unit extends CustomMarker {
this.#isLeftMouseDown = true;
this.#leftMouseDownEpoch = Date.now();
} else if (e.originalEvent?.button === 2) {
if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() !== ContextActionTarget.POINT) {
if (
getApp().getState() === OlympusState.IDLE ||
(getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getMap().getContextAction()?.getTarget() !== ContextActionTarget.POINT)
) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
@ -1375,6 +1372,19 @@ export abstract class Unit extends CustomMarker {
#onRightLongClick(e: any) {
console.log(`Right long click on ${this.getUnitName()}`);
if (getApp().getState() === OlympusState.IDLE) {
this.setSelected(!this.getSelected());
DomEvent.stop(e);
DomEvent.preventDefault(e);
e.originalEvent.stopImmediatePropagation();
window.setTimeout(() => {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.UNIT_CONTEXT_MENU);
UnitContextMenuRequestEvent.dispatch(this);
}, 200);
}
if (getApp().getState() === OlympusState.UNIT_CONTROL && !getApp().getMap().getContextAction()) {
DomEvent.stop(e);
DomEvent.preventDefault(e);
@ -1402,6 +1412,10 @@ export abstract class Unit extends CustomMarker {
}
#updateMarker() {
/* Delete existing trails */
this.#trailPolylines.forEach((polyline) => polyline.removeFrom(getApp().getMap()));
this.#trailPolylines = [];
this.updateVisibility();
/* Draw the minimap marker */
@ -1516,11 +1530,8 @@ export abstract class Unit extends CustomMarker {
/* Set bullseyes positions */
const bullseyes = getApp().getMissionManager().getBullseyes();
if (Object.keys(bullseyes).length > 0) {
computeBearingRangeString
const blueBullseye = `${computeBearingRangeString(bullseyes[2].getLatLng(), this.getPosition())}`;
const redBullseye = `${computeBearingRangeString(bullseyes[1].getLatLng(), this.getPosition())}`;
if (element.querySelector(".unit-blue-bullseye")) (<HTMLElement>element.querySelector(".unit-blue-bullseye")).innerText = `${blueBullseye}`;
if (element.querySelector(".unit-red-bullseye")) (<HTMLElement>element.querySelector(".unit-red-bullseye")).innerText = `${redBullseye}`;
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 */
@ -1534,6 +1545,35 @@ export abstract class Unit extends CustomMarker {
/* 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
);
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 (getApp().getMap().getOptions().AWACSMode) {
this.#trailPolylines = this.#trailPositions.map(
(latlng, idx) => new Polyline([latlng, latlng], { color: "#FFFFFF", opacity: 1 - (idx + 1) / TRAIL_LENGTH })
);
this.#trailPolylines.forEach((polyline) => polyline.addTo(getApp().getMap()));
}
}
}
@ -1807,10 +1847,8 @@ export abstract class AirUnit extends Unit {
showFuel: belongsToCommandedCoalition,
showAmmo: belongsToCommandedCoalition,
showSummary: belongsToCommandedCoalition || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)),
showCallsign: belongsToCommandedCoalition,
rotateToHeading: false,
showBullseyes: true,
showBRAA: true,
showCallsign: belongsToCommandedCoalition && (!getApp().getMap().getOptions().AWACSMode || this.getHuman()),
rotateToHeading: false
} as ObjectIconOptions;
}
@ -1898,10 +1936,8 @@ export class GroundUnit extends Unit {
showFuel: false,
showAmmo: false,
showSummary: false,
showCallsign: belongsToCommandedCoalition,
showCallsign: belongsToCommandedCoalition && (!getApp().getMap().getOptions().AWACSMode || this.getHuman()),
rotateToHeading: false,
showBullseyes: false,
showBRAA: false,
} as ObjectIconOptions;
}
@ -1969,10 +2005,8 @@ export class NavyUnit extends Unit {
showFuel: false,
showAmmo: false,
showSummary: false,
showCallsign: belongsToCommandedCoalition,
showCallsign: belongsToCommandedCoalition && (!getApp().getMap().getOptions().AWACSMode || this.getHuman()),
rotateToHeading: false,
showBullseyes: false,
showBRAA: false,
} as ObjectIconOptions;
}

View File

@ -1,16 +1,7 @@
import { LatLng, LatLngBounds } from "leaflet";
import { getApp } from "../olympusapp";
import { Unit } from "./unit";
import {
areaContains,
bearingAndDistanceToLatLng,
deg2rad,
getGroundElevation,
latLngToMercator,
mToFt,
mercatorToLatLng,
msToKnots,
} from "../other/utils";
import { AirUnit, Unit } from "./unit";
import { areaContains, bearingAndDistanceToLatLng, deg2rad, getGroundElevation, latLngToMercator, mToFt, mercatorToLatLng, msToKnots } from "../other/utils";
import { CoalitionPolygon } from "../map/coalitionarea/coalitionpolygon";
import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, OlympusState, UnitControlSubState } from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
@ -33,6 +24,8 @@ import {
UnitSelectedEvent,
} from "../events";
import { UnitDatabase } from "./databases/unitdatabase";
import * as turf from "@turf/turf";
import * as turfC from "@turf/clusters";
/** 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
@ -51,6 +44,7 @@ export class UnitsManager {
#unitDatabase: UnitDatabase;
#protectionCallback: (units: Unit[]) => void = (units) => {};
#AWACSReference: Unit | null = null;
#clusters: {[key: number]: Unit[]} = {};
constructor() {
this.#unitDatabase = new UnitDatabase();
@ -82,7 +76,7 @@ export class UnitsManager {
code: "KeyA",
ctrlKey: true,
shiftKey: false,
altKey: false
altKey: false,
})
.addShortcut("copyUnits", {
label: "Copy units",
@ -90,7 +84,7 @@ export class UnitsManager {
code: "KeyC",
ctrlKey: true,
shiftKey: false,
altKey: false
altKey: false,
})
.addShortcut("pasteUnits", {
label: "Paste units",
@ -98,7 +92,7 @@ export class UnitsManager {
code: "KeyV",
ctrlKey: true,
shiftKey: false,
altKey: false
altKey: false,
});
const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"];
@ -113,8 +107,9 @@ export class UnitsManager {
code: code,
shiftKey: false,
altKey: false,
ctrlKey: false
}).addShortcut(`hotgroup${idx + 1}add`, {
ctrlKey: false,
})
.addShortcut(`hotgroup${idx + 1}add`, {
label: `Hotgroup ${idx + 1} (Add to)`,
keyUpCallback: (ev: KeyboardEvent) => {
this.addToHotgroup(parseInt(ev.code.substring(5)));
@ -122,8 +117,9 @@ export class UnitsManager {
code: code,
shiftKey: true,
altKey: false,
ctrlKey: false
}).addShortcut(`hotgroup${idx + 1}set`, {
ctrlKey: false,
})
.addShortcut(`hotgroup${idx + 1}set`, {
label: `Hotgroup ${idx + 1} (Set)`,
keyUpCallback: (ev: KeyboardEvent) => {
this.setHotgroup(parseInt(ev.code.substring(5)));
@ -131,8 +127,9 @@ export class UnitsManager {
code: code,
ctrlKey: true,
altKey: false,
shiftKey: false
}).addShortcut(`hotgroup${idx + 1}also`, {
shiftKey: false,
})
.addShortcut(`hotgroup${idx + 1}also`, {
label: `Hotgroup ${idx + 1} (Select also)`,
keyUpCallback: (ev: KeyboardEvent) => {
this.selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false);
@ -140,7 +137,7 @@ export class UnitsManager {
code: code,
ctrlKey: true,
shiftKey: true,
altKey: false
altKey: false,
});
});
@ -280,6 +277,9 @@ export class UnitsManager {
if (this.#units[ID].getSelected()) this.#units[ID].drawLines();
}
/* Compute the base clusters */
this.#clusters = this.computeClusters();
return updateTime;
}
@ -1548,13 +1548,37 @@ export class UnitsManager {
setAWACSReference(ID) {
this.#AWACSReference = this.#units[ID] ?? null;
AWACSReferenceChangedEvent.dispatch(this.#AWACSReference)
AWACSReferenceChangedEvent.dispatch(this.#AWACSReference);
}
getAWACSReference() {
return this.#AWACSReference;
}
computeClusters(filter: (unit: Unit) => boolean = (unit) => true, distance: number = 5 /* km */) {
let units = Object.values(this.#units)
.filter((unit) => unit.getAlive() && unit instanceof AirUnit)
.filter(filter);
var geojson = turf.featureCollection(units.map((unit) => turf.point([unit.getPosition().lng, unit.getPosition().lat])));
//@ts-ignore
var clustered = turf.clustersDbscan(geojson, distance, { minPoints: 1 });
let clusters: {[key: number]: Unit[]} = {};
clustered.features.forEach((feature, idx) => {
if (clusters[feature.properties.cluster] === undefined)
clusters[feature.properties.cluster] = [] as Unit[];
clusters[feature.properties.cluster].push(units[idx]);
})
return clusters;
}
getClusters() {
return this.#clusters;
}
/***********************************************/
#onUnitSelection(unit: Unit) {
if (this.getSelectedUnits().length > 0) {

View File

@ -178,7 +178,7 @@ export class Weapon extends CustomMarker {
var unitIcon = document.createElement("div");
unitIcon.classList.add("unit-icon");
var img = document.createElement("img");
img.src = `/vite/images/units/${this.getMarkerCategory()}.svg`;
img.src = `/vite/images/units/map/${getApp().getMap().getOptions().AWACSMode ? "awacs" : "normal"}/${this.getCoalition()}/${this.getMarkerCategory()}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
unitIcon.toggleAttribute("data-rotate-to-heading", this.getIconOptions().rotateToHeading);