setReferenceSystem("LatLngDMS")}
+ >
+
+
+ {props.location.lat >= 0 ? "N" : "S"}
+
+ {zeroAppend(props.location.lat, 3, true, 6)}
+
+
+
+ {props.location.lng >= 0 ? "E" : "W"}
+
+ {zeroAppend(props.location.lng, 3, true, 6)}
+
+
+ );
+ } else if (referenceSystem === "LatLngDMS") {
+ return (
+ setReferenceSystem("MGRS")}
+ >
+
+
+ {props.location.lat >= 0 ? "N" : "S"}
+
+ {ConvertDDToDMS(props.location.lat, false)}
+
+
+
+ {props.location.lng >= 0 ? "E" : "W"}
+
+ {ConvertDDToDMS(props.location.lng, false)}
+
+
+ );
+ } else {
+ }
+}
diff --git a/frontend/react/src/ui/panels/jtacmenu.tsx b/frontend/react/src/ui/panels/jtacmenu.tsx
new file mode 100644
index 00000000..6d35cc21
--- /dev/null
+++ b/frontend/react/src/ui/panels/jtacmenu.tsx
@@ -0,0 +1,294 @@
+import React, { useEffect, useState } from "react";
+import { Menu } from "./components/menu";
+import { getApp } from "../../olympusapp";
+import { IDLE, SELECT_JTAC_ECHO, SELECT_JTAC_IP, SELECT_JTAC_TARGET } from "../../constants/constants";
+import { LatLng } from "leaflet";
+import { Unit } from "../../unit/unit";
+import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
+import { bearing, point } from "turf";
+import { ConvertDDToDMS, latLngToMGRS, mToFt, zeroAppend } from "../../other/utils";
+import { FaMousePointer } from "react-icons/fa";
+import { OlLocation } from "../components/ollocation";
+import { FaBullseye } from "react-icons/fa6";
+
+export function JTACMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
+ const [referenceSystem, setReferenceSystem] = useState("LatLngDec");
+ const [targetLocation, setTargetLocation] = useState(null as null | LatLng);
+ const [targetUnit, setTargetUnit] = useState(null as null | Unit);
+ const [IP, setIP] = useState(null as null | LatLng);
+ const [ECHO, setECHO] = useState(null as null | LatLng);
+ const [mapState, setMapState] = useState(IDLE);
+ const [callsign, setCallsign] = useState("Eyeball");
+ const [humanUnits, setHumanUnits] = useState([] as Unit[]);
+ const [attacker, setAttacker] = useState(null as null | Unit);
+ const [type, setType] = useState("Type 1");
+
+ useEffect(() => {
+ document.addEventListener("selectJTACTarget", (ev: CustomEventInit) => {
+ setTargetLocation(null);
+ setTargetUnit(null);
+
+ if (ev.detail.location) setTargetLocation(ev.detail.location);
+ if (ev.detail.unit) setTargetUnit(ev.detail.unit);
+ });
+
+ document.addEventListener("selectJTACECHO", (ev: CustomEventInit) => {
+ setECHO(ev.detail);
+ });
+
+ document.addEventListener("selectJTACIP", (ev: CustomEventInit) => {
+ setIP(ev.detail);
+ });
+
+ document.addEventListener("mapStateChanged", (ev: CustomEventInit) => {
+ setMapState(ev.detail);
+ if (ev.detail === SELECT_JTAC_TARGET) {
+ setTargetLocation(null);
+ setTargetUnit(null);
+ }
+ });
+ }, []);
+
+ useEffect(() => {
+ if (getApp()) setHumanUnits(Object.values(getApp().getUnitsManager().getUnits()).filter((unit) => unit.getAlive()));
+ }, [targetLocation, targetUnit]);
+
+ let IPPosition = "";
+ if (IP && ECHO) {
+ let dist = Math.round(IP.distanceTo(ECHO) / 1852);
+ let bear = bearing(point([ECHO.lng, ECHO.lat]), point([IP.lng, IP.lat]));
+ IPPosition = ["A", "AB", "B", "BC", "C", "CD", "D", "DA"][Math.round((bear > 0 ? bear : bear + 360) / 45)] + String(dist);
+ }
+
+ let IPtoTargetBear = 0;
+ let IPtoTargetDist = 0;
+
+ if (IP) {
+ let location = targetUnit ? targetUnit.getPosition() : targetLocation;
+ if (location) {
+ IPtoTargetDist = Math.round(IP.distanceTo(location) / 1852);
+ IPtoTargetBear = bearing(point([IP.lng, IP.lat]), point([location.lng, location.lat]));
+ if (IPtoTargetBear < 0) IPtoTargetBear += 360;
+ IPtoTargetBear = Math.round(IPtoTargetBear);
+ }
+ }
+
+ let targetAltitude = targetUnit?.getPosition().alt ?? 0;
+ let targetPosition = (targetUnit ? targetUnit.getPosition() : targetLocation) ?? new LatLng(0, 0);
+
+ return (
+
diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx
index 51900483..fc4381ba 100644
--- a/frontend/react/src/ui/ui.tsx
+++ b/frontend/react/src/ui/ui.tsx
@@ -27,6 +27,7 @@ import { FormationMenu } from "./panels/formationmenu";
import { Unit } from "../unit/unit";
import { ProtectionPrompt } from "./modals/protectionprompt";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
+import { JTACMenu } from "./panels/jtacmenu";
export type OlympusUIState = {
mainMenuVisible: boolean;
@@ -52,6 +53,7 @@ export function UI() {
const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false);
const [formationMenuVisible, setFormationMenuVisible] = useState(false);
const [unitExplosionMenuVisible, setUnitExplosionMenuVisible] = useState(false);
+ const [JTACMenuVisible, setJTACMenuVisible] = useState(false);
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
const [checkingPassword, setCheckingPassword] = useState(false);
@@ -184,6 +186,7 @@ export function UI() {
optionsMenuVisible: optionsMenuVisible,
airbaseMenuVisible: airbaseMenuVisible,
audioMenuVisible: audioMenuVisible,
+ JTACMenuVisible: JTACMenuVisible,
mapOptions: mapOptions,
mapHiddenTypes: mapHiddenTypes,
mapSources: mapSources,
@@ -200,6 +203,7 @@ export function UI() {
setMeasureMenuVisible: setMeasureMenuVisible,
setOptionsMenuVisible: setOptionsMenuVisible,
setAirbaseMenuVisible: setAirbaseMenuVisible,
+ setJTACMenuVisible: setJTACMenuVisible,
setAudioMenuVisible: setAudioMenuVisible,
toggleMainMenuVisible: () => {
hideAllMenus();
@@ -233,6 +237,10 @@ export function UI() {
hideAllMenus();
setAudioMenuVisible(!audioMenuVisible);
},
+ toggleJTACMenuVisible: () => {
+ hideAllMenus();
+ setJTACMenuVisible(!JTACMenuVisible);
+ },
}}
>
@@ -289,6 +297,7 @@ export function UI() {
setAudioMenuVisible(false)} />
setFormationMenuVisible(false)} />
setUnitExplosionMenuVisible(false)} />
+ setJTACMenuVisible(false)} />
diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts
index d58ef9bf..b536213e 100644
--- a/frontend/react/src/unit/unit.ts
+++ b/frontend/react/src/unit/unit.ts
@@ -39,6 +39,7 @@ import {
MAX_SHOTS_SCATTER,
SHOTS_SCATTER_DEGREES,
CONTEXT_ACTION,
+ SELECT_JTAC_TARGET,
} from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { groundUnitDatabase } from "./databases/groundunitdatabase";
@@ -1384,6 +1385,9 @@ export abstract class Unit extends CustomMarker {
getApp().getUnitsManager().deselectAllUnits();
this.setSelected(!this.getSelected());
}
+ } else if (getApp().getMap().getState() === SELECT_JTAC_TARGET) {
+ document.dispatchEvent(new CustomEvent("selectJTACTarget", {detail: {unit: this}}))
+ getApp().getMap().setState(IDLE)
}
}