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

@@ -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 />