Started working on JTAC tools

This commit is contained in:
Davide Passoni 2024-10-18 20:17:04 +02:00
parent 58f114bba0
commit 0c5139f5ee
14 changed files with 612 additions and 22 deletions

View File

@ -61,7 +61,9 @@ export class AudioManager {
let wsAddress = res ? res[1] : this.#address;
if (this.#address.includes("https")) this.#socket = new WebSocket(`wss://${wsAddress}/${this.#endpoint}`);
else this.#socket = new WebSocket(`wss://refugees.dcsolympus.com/audio`);
else this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`);
this.#socket = new WebSocket(`wss://refugees.dcsolympus.com/audio`); // TODO: remove, used for testing!
/* Log the opening of the connection */
this.#socket.addEventListener("open", (event) => {

View File

@ -245,6 +245,9 @@ export const CONTEXT_ACTION = "Context action";
export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area polygon";
export const COALITIONAREA_DRAW_CIRCLE = "Draw Coalition Area circle";
export const COALITIONAREA_EDIT = "Edit Coalition Area";
export const SELECT_JTAC_TARGET = "Select JTAC target"
export const SELECT_JTAC_ECHO = "Select JTAC echo point"
export const SELECT_JTAC_IP = "Select JTAC IP"
export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"];
export const IADSDensities: { [key: string]: number } = {

View File

@ -9,6 +9,7 @@ export const EventsContext = createContext({
setOptionsMenuVisible: (e: boolean) => {},
setAirbaseMenuVisible: (e: boolean) => {},
setAudioMenuVisible: (e: boolean) => {},
setJTACMenuVisible: (e: boolean) => {},
toggleMainMenuVisible: () => {},
toggleSpawnMenuVisible: () => {},
toggleUnitControlMenuVisible: () => {},
@ -17,6 +18,7 @@ export const EventsContext = createContext({
toggleOptionsMenuVisible: () => {},
toggleAirbaseMenuVisible: () => {},
toggleAudioMenuVisible: () => {},
toggleJTACMenuVisible: () => {},
});
export const EventsProvider = EventsContext.Provider;

View File

@ -135,4 +135,14 @@
background-image: url("/vite/images/markers/target.svg");
height: 100%;
width: 100%;
}
.ol-text-icon {
color: #111111;
text-align: center;
padding: 7px;
border-radius: 999px;
font-weight: bold;
border: 2px solid black;
font-size: 14px;
}

View File

@ -21,6 +21,9 @@ import {
COALITIONAREA_DRAW_CIRCLE,
NOT_INITIALIZED,
SPAWN_EFFECT,
SELECT_JTAC_TARGET,
SELECT_JTAC_ECHO,
SELECT_JTAC_IP,
} from "../constants/constants";
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
import { MapHiddenTypes, MapOptions } from "../types/types";
@ -37,6 +40,8 @@ import { CoalitionCircle } from "./coalitionarea/coalitioncircle";
import { initDraggablePath } from "./coalitionarea/draggablepath";
import { faDrawPolygon, faHandPointer, faJetFighter, faMap } from "@fortawesome/free-solid-svg-icons";
import { ExplosionMarker } from "./markers/explosionmarker";
import { TextMarker } from "./markers/textmarker";
import { TargetMarker } from "./markers/targetmarker";
/* Register the handler for the box selection */
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
@ -114,6 +119,12 @@ export class Map extends L.Map {
#temporaryMarkers: TemporaryUnitMarker[] = [];
#currentSpawnMarker: TemporaryUnitMarker | null = null;
/* JTAC tools */
#ECHOPoint: TextMarker | null = null;
#IPPoint: TextMarker | null = null;
#targetPoint: TargetMarker | null = null;
#IPToTargetLine: L.Polygon | null = null;
/**
*
* @param ID - the ID of the HTML element which will contain the map
@ -249,6 +260,57 @@ export class Map extends L.Map {
this.#broadcastPosition();
});
document.addEventListener("selectJTACECHO", (ev: CustomEventInit) => {
if (!this.#ECHOPoint) {
this.#ECHOPoint = new TextMarker(ev.detail, "BP", "rgb(37 99 235)", { interactive: true, draggable: true });
this.#ECHOPoint.addTo(this);
this.#ECHOPoint.on("dragstart", (event) => {
event.target.options["freeze"] = true;
});
this.#ECHOPoint.on("dragend", (event) => {
document.dispatchEvent(new CustomEvent("selectJTACECHO", { detail: this.#ECHOPoint?.getLatLng() }));
event.target.options["freeze"] = false;
});
} else this.#ECHOPoint.setLatLng(ev.detail);
});
document.addEventListener("selectJTACIP", (ev: CustomEventInit) => {
if (!this.#IPPoint) {
this.#IPPoint = new TextMarker(ev.detail, "IP", "rgb(168 85 247)", { interactive: true, draggable: true });
this.#IPPoint.addTo(this);
this.#IPPoint.on("dragstart", (event) => {
event.target.options["freeze"] = true;
});
this.#IPPoint.on("dragend", (event) => {
document.dispatchEvent(new CustomEvent("selectJTACIP", { detail: this.#IPPoint?.getLatLng() }));
event.target.options["freeze"] = false;
});
} else this.#IPPoint.setLatLng(ev.detail);
this.#drawIPToTargetLine();
});
document.addEventListener("selectJTACTarget", (ev: CustomEventInit) => {
if (ev.detail.location) {
if (!this.#targetPoint) {
this.#targetPoint = new TargetMarker(ev.detail.location, { interactive: true, draggable: true });
this.#targetPoint.addTo(this);
this.#targetPoint.on("dragstart", (event) => {
event.target.options["freeze"] = true;
});
this.#targetPoint.on("dragend", (event) => {
document.dispatchEvent(new CustomEvent("selectJTACTarget", { detail: {location: this.#targetPoint?.getLatLng() }}));
event.target.options["freeze"] = false;
});
} else this.#targetPoint.setLatLng(ev.detail.location);
} else {
this.#targetPoint?.removeFrom(this);
this.#targetPoint = null;
}
this.#drawIPToTargetLine();
});
/* Pan interval */
this.#panInterval = window.setInterval(() => {
if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft)
@ -368,7 +430,11 @@ export class Map extends L.Map {
this.#spawnRequestTable = options?.spawnRequestTable ?? null;
console.log(`Spawn request table:`);
console.log(this.#spawnRequestTable);
this.#currentSpawnMarker = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral")
this.#currentSpawnMarker = new TemporaryUnitMarker(
new L.LatLng(0, 0),
this.#spawnRequestTable?.unit.unitType ?? "",
this.#spawnRequestTable?.coalition ?? "neutral"
);
this.#currentSpawnMarker.addTo(this);
} else if (this.#state === SPAWN_EFFECT) {
this.deselectAllCoalitionAreas();
@ -549,6 +615,60 @@ export class Map extends L.Map {
text: "Move map location",
},
];
} else if (this.#state === SELECT_JTAC_TARGET) {
return [
{
actions: [touch ? faHandPointer : "LMB"],
target: faMap,
text: "Set unit/location as target",
},
{
actions: [touch ? faHandPointer : "LMB", 2],
target: faMap,
text: "Exit selection mode",
},
{
actions: [touch ? faHandPointer : "LMB", "Drag"],
target: faMap,
text: "Move map location",
},
];
} else if (this.#state === SELECT_JTAC_ECHO) {
return [
{
actions: [touch ? faHandPointer : "LMB"],
target: faMap,
text: "Set location as ECHO point",
},
{
actions: [touch ? faHandPointer : "LMB", 2],
target: faMap,
text: "Exit selection mode",
},
{
actions: [touch ? faHandPointer : "LMB", "Drag"],
target: faMap,
text: "Move map location",
},
];
} else if (this.#state === SELECT_JTAC_IP) {
return [
{
actions: [touch ? faHandPointer : "LMB"],
target: faMap,
text: "Set location as IP point",
},
{
actions: [touch ? faHandPointer : "LMB", 2],
target: faMap,
text: "Exit selection mode",
},
{
actions: [touch ? faHandPointer : "LMB", "Drag"],
target: faMap,
text: "Move map location",
},
];
} else {
return [];
}
@ -846,7 +966,7 @@ export class Map extends L.Map {
this.setState(COALITIONAREA_EDIT);
} else {
this.setState(IDLE);
document.dispatchEvent(new CustomEvent("hideAllMenus"))
document.dispatchEvent(new CustomEvent("hideAllMenus"));
}
}
@ -881,9 +1001,9 @@ export class Map extends L.Map {
}
} else if (this.#state === SPAWN_EFFECT) {
if (e.originalEvent.button != 2 && this.#effectRequestTable !== null) {
getApp().getServerManager().spawnExplosion(50, 'normal', pressLocation);
getApp().getServerManager().spawnExplosion(50, "normal", pressLocation);
}
} else if (this.#state === COALITIONAREA_DRAW_POLYGON) {
} else if (this.#state === COALITIONAREA_DRAW_POLYGON) {
const selectedArea = this.getSelectedCoalitionArea();
if (selectedArea && selectedArea instanceof CoalitionPolygon) {
selectedArea.addTemporaryLatLng(pressLocation);
@ -908,12 +1028,21 @@ export class Map extends L.Map {
}
}
} else if (this.#state === CONTEXT_ACTION) {
if (e.type === 'touchstart' || e.originalEvent.buttons === 1) {
if (e.type === "touchstart" || e.originalEvent.buttons === 1) {
if (this.#contextAction !== null) this.executeContextAction(null, pressLocation);
else this.setState(IDLE);
} else if (e.originalEvent.buttons === 2) {
if (this.#defaultContextAction !== null) this.executeDefaultContextAction(null, pressLocation);
}
} else if (this.#state === SELECT_JTAC_TARGET) {
document.dispatchEvent(new CustomEvent("selectJTACTarget", { detail: { location: pressLocation } }));
this.setState(IDLE);
} else if (this.#state === SELECT_JTAC_ECHO) {
document.dispatchEvent(new CustomEvent("selectJTACECHO", { detail: pressLocation }));
this.setState(IDLE);
} else if (this.#state === SELECT_JTAC_IP) {
document.dispatchEvent(new CustomEvent("selectJTACIP", { detail: pressLocation }));
this.setState(IDLE);
} else {
}
}
@ -1058,4 +1187,13 @@ export class Map extends L.Map {
this.#cameraOptionsXmlHttp.timeout = 500;
this.#cameraOptionsXmlHttp.send("");
}
#drawIPToTargetLine() {
if (this.#targetPoint && this.#IPPoint) {
if (!this.#IPToTargetLine) {
this.#IPToTargetLine = new L.Polygon([this.#targetPoint.getLatLng(), this.#IPPoint.getLatLng()]);
this.#IPToTargetLine.addTo(this);
} else this.#IPToTargetLine.setLatLngs([this.#targetPoint.getLatLng(), this.#IPPoint.getLatLng()]);
}
}
}

View File

@ -0,0 +1,32 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
export class TextMarker extends CustomMarker {
#label: string = "";
#backgroundColor: string = "";
constructor(latlng: LatLngExpression, label: string, backgroundColor: string, options?: MarkerOptions) {
super(latlng, options);
this.setZIndexOffset(9999);
this.#label = label;
this.#backgroundColor = backgroundColor;
}
createIcon() {
this.setIcon(
new DivIcon({
iconSize: [40, 40],
iconAnchor: [20, 20],
className: "leaflet-text-marker",
})
);
var el = document.createElement("div");
el.classList.add("ol-text-icon")
el.style.backgroundColor = this.#backgroundColor;
this.getElement()?.appendChild(el);
el.innerHTML = this.#label;
}
}

View File

@ -52,14 +52,13 @@ export function bearingAndDistanceToLatLng(lat: number, lon: number, brng: numbe
}
export function ConvertDDToDMS(D: number, lng: boolean) {
var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N";
var deg = 0 | (D < 0 ? (D = -D) : D);
var min = 0 | (((D += 1e-9) % 1) * 60);
var sec = (0 | (((D * 60) % 1) * 6000)) / 100;
var dec = Math.round((sec - Math.floor(sec)) * 100);
var sec = Math.floor(sec);
if (lng) return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + '"';
else return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + '"';
if (lng) return zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + '"';
else return zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + '"';
}
export function dataPointMap(container: HTMLElement, data: any) {
@ -122,7 +121,7 @@ export const zeroAppend = function (num: number, places: number, decimal: boolea
export const zeroPad = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
string += "0";
string = "0" + string;
}
return string;
};

View File

@ -10,6 +10,7 @@ export const StateContext = createContext({
optionsMenuVisible: false,
airbaseMenuVisible: false,
audioMenuVisible: false,
JTACMenuVisible: false,
mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS,
mapOptions: MAP_OPTIONS_DEFAULTS,
mapSources: [] as string[],

View File

@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
export function OlDropdown(props: {
disableAutoClose?: boolean;
className?: string;
leftIcon?: IconProp;
rightIcon?: IconProp;
@ -103,12 +104,9 @@ export function OlDropdown(props: {
`}
type="button"
>
{props.leftIcon && (
<FontAwesomeIcon
icon={props.leftIcon}
className={`mr-3`}
/>
)}
{props.leftIcon && <FontAwesomeIcon icon={props.leftIcon} className={`
mr-3
`} />}
<span className="overflow-hidden text-ellipsis text-nowrap">{props.label ?? ""}</span>
<svg
className={`
@ -140,6 +138,9 @@ export function OlDropdown(props: {
h-fit w-full text-sm text-gray-700
dark:text-gray-200
`}
onClick={() => {
props.disableAutoClose !== true && setOpen(false);
}}
>
{props.children}
</div>
@ -149,11 +150,7 @@ export function OlDropdown(props: {
}
/* Conveniency Component for dropdown elements */
export function OlDropdownItem(props: {
onClick?: () => void;
className?: string;
children?: string | JSX.Element | JSX.Element[];
}) {
export function OlDropdownItem(props: { onClick?: () => void; className?: string; children?: string | JSX.Element | JSX.Element[] }) {
return (
<button
onClick={props.onClick ?? (() => {})}

View File

@ -0,0 +1,93 @@
import React, { useState } from "react";
import { LatLng } from "leaflet";
import { ConvertDDToDMS, latLngToMGRS, latLngToUTM, zeroAppend } from "../../other/utils";
export function OlLocation(props: { location: LatLng; className?: string; referenceSystem?: string; onClick?: () => void }) {
const [referenceSystem, setReferenceSystem] = props.referenceSystem ? [props.referenceSystem, () => {}] : useState("LatLngDec");
const MGRS = latLngToMGRS(props.location.lat, props.location.lng, 6);
if (referenceSystem === "MGRS") {
return (
<div
className={`
${props.className ?? ""}
my-auto cursor-pointer bg-olympus-400 p-2 text-white
`}
onClick={props.onClick ? props.onClick : () => setReferenceSystem("LatLngDec")}
>
<span
className={`
mr-2 rounded-sm bg-white px-1 text-center font-bold text-olympus-700
`}
>
MGRS
</span>
{MGRS ? MGRS.string : "Error"}
</div>
);
} else if (referenceSystem === "LatLngDec") {
return (
<div
className={`
${props.className ?? ""}
my-auto flex cursor-pointer justify-between gap-2 bg-olympus-400 p-2
text-white
`}
onClick={props.onClick ? props.onClick : () => setReferenceSystem("LatLngDMS")}
>
<div className="flex gap-2">
<span
className={`
w-5 rounded-sm bg-white text-center font-bold text-olympus-700
`}
>
{props.location.lat >= 0 ? "N" : "S"}
</span>
{zeroAppend(props.location.lat, 3, true, 6)}
</div>
<div className="flex w-[50%] gap-2">
<span
className={`
w-5 rounded-sm bg-white text-center font-bold text-olympus-700
`}
>
{props.location.lng >= 0 ? "E" : "W"}
</span>
{zeroAppend(props.location.lng, 3, true, 6)}
</div>
</div>
);
} else if (referenceSystem === "LatLngDMS") {
return (
<div
className={`
${props.className ?? ""}
my-auto flex cursor-pointer justify-between gap-2 bg-olympus-400 p-2
text-white
`}
onClick={props.onClick ? props.onClick : () => setReferenceSystem("MGRS")}
>
<div className="flex gap-2">
<span
className={`
w-5 rounded-sm bg-white text-center font-bold text-olympus-700
`}
>
{props.location.lat >= 0 ? "N" : "S"}
</span>
{ConvertDDToDMS(props.location.lat, false)}
</div>
<div className="flex w-[50%] gap-2">
<span
className={`
w-5 rounded-sm bg-white text-center font-bold text-olympus-700
`}
>
{props.location.lng >= 0 ? "E" : "W"}
</span>
{ConvertDDToDMS(props.location.lng, false)}
</div>
</div>
);
} else {
}
}

View File

@ -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 (
<Menu title={"JTAC Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<div
className={`
flex flex-col gap-2 p-4 font-normal text-gray-800
dark:text-white
`}
>
<>
<div className="flex">
<span className="my-auto min-w-32 text-nowrap">JTAC Callsign</span>
<input
className={`
block h-10 w-full border-[2px] bg-gray-50 py-2.5 text-center
text-sm text-gray-900
dark:border-gray-700 dark:bg-olympus-600 dark:text-white
dark:placeholder-gray-400 dark:focus:border-blue-700
dark:focus:ring-blue-700
focus:border-blue-700 focus:ring-blue-500
`}
value={callsign}
onChange={(ev) => setCallsign(ev.target.value)}
></input>
</div>
<div className="flex">
<span
className={`
my-auto h-full min-w-10 text-nowrap p-2 text-center
`}
>
BP
</span>
<OlLocation
location={ECHO ?? new LatLng(0, 0)}
className={`
h-full w-full rounded-l-lg
${!ECHO ? "text-red-600" : ""}
`}
onClick={() => {
if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec");
else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS");
else setReferenceSystem("MGRS");
}}
referenceSystem={referenceSystem}
/>
<button
type="button"
onClick={() => {
getApp().getMap().setState(SELECT_JTAC_ECHO);
}}
className={`
rounded-r-md bg-blue-700 px-3 py-2.5 text-md font-medium
text-white
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
<FaMousePointer />
</button>
</div>
<div className="flex">
<span
className={`
my-auto h-full min-w-10 text-nowrap p-2 text-center
`}
>
IP
</span>
<OlLocation
location={IP ?? new LatLng(0, 0)}
className={`
h-full w-full rounded-l-lg
${!IP ? "text-red-600" : ""}
`}
onClick={() => {
if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec");
else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS");
else setReferenceSystem("MGRS");
}}
referenceSystem={referenceSystem}
/>
<button
type="button"
onClick={() => {
getApp().getMap().setState(SELECT_JTAC_IP);
}}
className={`
rounded-r-lg bg-blue-700 px-3 py-2.5 text-md font-medium
text-white
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
<FaMousePointer />
</button>
</div>
<div className="flex">
<span
className={`
my-auto h-full min-w-10 text-nowrap p-3 text-center
`}
>
<FaBullseye />
</span>
<OlLocation
location={(targetUnit ? targetUnit.getPosition() : targetLocation) ?? new LatLng(0, 0)}
className={`
h-full w-full rounded-l-lg
${!(targetUnit || targetLocation) ? "text-red-600" : ""}
`}
onClick={() => {
if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec");
else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS");
else setReferenceSystem("MGRS");
}}
referenceSystem={referenceSystem}
/>
<button
type="button"
onClick={() => {
getApp().getMap().setState(SELECT_JTAC_TARGET);
}}
className={`
rounded-r-lg bg-blue-700 px-3 py-2.5 text-md font-medium
text-white
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
<FaMousePointer />
</button>
</div>
<div className="flex gap-2">
<span className="my-auto min-w-32 text-nowrap">Attacker:</span>{" "}
<OlDropdown
label={attacker ? attacker.getUnitName() : "Select unit"}
className={`w-full truncate`}
>
{humanUnits.map((unit, idx) => {
return (
<OlDropdownItem
key={idx}
onClick={() => {
setAttacker(unit);
}}
className="truncate"
>
<span className="truncate">{unit.getUnitName()}</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
</>
{(targetLocation || targetUnit) && (
<div className="flex flex-col gap-2">
<span>9 Line</span>
<div className="flex flex-col">
<span className="italic">
{attacker?.getUnitName()}, {callsign}.
</span>
<span className="italic">
This will be a {type.toLowerCase()} attack, {targetLocation ? "bombs on coordinates" : "bombs on target"}.
</span>
{IP ? (
<span className="italic">
<span className="font-bold text-purple-500">(1, 2, 3)</span> Entry keyhole {IPPosition}, heading {IPtoTargetBear}, {IPtoTargetDist} miles
</span>
) : (
<span className="italic">
<span className="font-bold text-purple-500">(1, 2, 3)</span> Not applicable
</span>
)}
<span className="italic">
<span className={`font-bold text-purple-500`}>(4)</span> Elevation {Math.round(mToFt(targetAltitude))}ft
</span>
<span className="italic">
<span className="font-bold text-purple-500">(5)</span> Target is {targetUnit ? targetUnit.getType() : "insert description"}
</span>
<span className="italic">
<span className="font-bold text-purple-500">(6)</span> Located{" "}
{referenceSystem === "LatLngDMS" && (
<>
{(targetPosition.lat >= 0 ? "N" : "S") + ConvertDDToDMS(targetPosition.lat, false)}{" "}
{(targetPosition.lng >= 0 ? "E" : "W") + ConvertDDToDMS(targetPosition.lng, true)}
</>
)}
{referenceSystem === "LatLngDec" && (
<>
{(targetPosition.lat >= 0 ? "N" : "S") + zeroAppend(targetPosition.lat, 3, true, 6)}{" "}
{(targetPosition.lng >= 0 ? "E" : "W") + zeroAppend(targetPosition.lng, 3, true, 6)}
</>
)}
{referenceSystem === "MGRS" && (
<>
{latLngToMGRS(targetPosition.lat, targetPosition.lng, 6).string}
</>
)}
</span>
<span className="italic">
<span className="font-bold text-purple-500">(7)</span> Marked by XXX
</span>
<span className="italic">
<span className="font-bold text-purple-500">(8)</span> Friendlies XXX
</span>
</div>
</div>
)}
</div>
</Menu>
);
}

View File

@ -1,6 +1,6 @@
import React from "react";
import { OlStateButton } from "../components/olstatebutton";
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faRadio, faVolumeHigh } from "@fortawesome/free-solid-svg-icons";
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faRadio, faVolumeHigh, faJ } from "@fortawesome/free-solid-svg-icons";
import { EventsConsumer } from "../../eventscontext";
import { StateConsumer } from "../../statecontext";
import { IDLE } from "../../constants/constants";
@ -58,6 +58,12 @@ export function SideBar() {
icon={faVolumeHigh}
tooltip="Hide/show audio menu"
></OlStateButton>
<OlStateButton
onClick={events.toggleJTACMenuVisible}
checked={appState.JTACMenuVisible}
icon={faJ}
tooltip="Hide/show JTAC menu"
></OlStateButton>
</div>
</div>
<div className="flex w-16 flex-wrap content-end justify-center p-4">

View File

@ -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);
},
}}
>
<Header />
@ -289,6 +297,7 @@ export function UI() {
<AudioMenu open={audioMenuVisible} onClose={() => setAudioMenuVisible(false)} />
<FormationMenu open={formationMenuVisible} leader={formationLeader} wingmen={formationWingmen} onClose={() => setFormationMenuVisible(false)} />
<UnitExplosionMenu open={unitExplosionMenuVisible} units={unitExplosionUnits} onClose={() => setUnitExplosionMenuVisible(false)} />
<JTACMenu open={JTACMenuVisible} onClose={() => setJTACMenuVisible(false)} />
<MiniMapPanel />
<ControlsPanel />

View File

@ -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)
}
}