mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Started working on JTAC tools
This commit is contained in:
parent
58f114bba0
commit
0c5139f5ee
@ -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) => {
|
||||
|
||||
@ -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 } = {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
frontend/react/src/map/markers/textmarker.ts
Normal file
32
frontend/react/src/map/markers/textmarker.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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[],
|
||||
|
||||
@ -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 ?? (() => {})}
|
||||
|
||||
93
frontend/react/src/ui/components/ollocation.tsx
Normal file
93
frontend/react/src/ui/components/ollocation.tsx
Normal 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 {
|
||||
}
|
||||
}
|
||||
294
frontend/react/src/ui/panels/jtacmenu.tsx
Normal file
294
frontend/react/src/ui/panels/jtacmenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user