diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 213d7dce..c6a49212 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -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) => { diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 0cc857a7..bc38ab77 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -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 } = { diff --git a/frontend/react/src/eventscontext.tsx b/frontend/react/src/eventscontext.tsx index b6864f78..0bead943 100644 --- a/frontend/react/src/eventscontext.tsx +++ b/frontend/react/src/eventscontext.tsx @@ -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; diff --git a/frontend/react/src/map/map.css b/frontend/react/src/map/map.css index 5b74dd17..26cb4710 100644 --- a/frontend/react/src/map/map.css +++ b/frontend/react/src/map/map.css @@ -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; } \ No newline at end of file diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 841ec3b2..7541a28c 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -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()]); + } + } } diff --git a/frontend/react/src/map/markers/textmarker.ts b/frontend/react/src/map/markers/textmarker.ts new file mode 100644 index 00000000..2746fd35 --- /dev/null +++ b/frontend/react/src/map/markers/textmarker.ts @@ -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; + } +} diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 87949c54..1d1457d2 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -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; }; diff --git a/frontend/react/src/statecontext.tsx b/frontend/react/src/statecontext.tsx index 8e0dc182..064f3c03 100644 --- a/frontend/react/src/statecontext.tsx +++ b/frontend/react/src/statecontext.tsx @@ -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[], diff --git a/frontend/react/src/ui/components/oldropdown.tsx b/frontend/react/src/ui/components/oldropdown.tsx index 2768949e..a0999707 100644 --- a/frontend/react/src/ui/components/oldropdown.tsx +++ b/frontend/react/src/ui/components/oldropdown.tsx @@ -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 && ( - - )} + {props.leftIcon && } {props.label ?? ""} { + props.disableAutoClose !== true && setOpen(false); + }} > {props.children} @@ -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 (
setReferenceSystem("LatLngDec")} + > + + MGRS + + {MGRS ? MGRS.string : "Error"} +
+ ); + } else if (referenceSystem === "LatLngDec") { + return ( +
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 ( + +
+ <> +
+ JTAC Callsign + setCallsign(ev.target.value)} + > +
+
+ + BP + + { + if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec"); + else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS"); + else setReferenceSystem("MGRS"); + }} + referenceSystem={referenceSystem} + /> + +
+
+ + IP + + { + if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec"); + else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS"); + else setReferenceSystem("MGRS"); + }} + referenceSystem={referenceSystem} + /> + +
+
+ + + + { + if (referenceSystem === "MGRS") setReferenceSystem("LatLngDec"); + else if (referenceSystem === "LatLngDec") setReferenceSystem("LatLngDMS"); + else setReferenceSystem("MGRS"); + }} + referenceSystem={referenceSystem} + /> + +
+ +
+ Attacker:{" "} + + {humanUnits.map((unit, idx) => { + return ( + { + setAttacker(unit); + }} + className="truncate" + > + {unit.getUnitName()} + + ); + })} + +
+ + {(targetLocation || targetUnit) && ( +
+ 9 Line +
+ + {attacker?.getUnitName()}, {callsign}. + + + This will be a {type.toLowerCase()} attack, {targetLocation ? "bombs on coordinates" : "bombs on target"}. + + {IP ? ( + + (1, 2, 3) Entry keyhole {IPPosition}, heading {IPtoTargetBear}, {IPtoTargetDist} miles + + ) : ( + + (1, 2, 3) Not applicable + + )} + + (4) Elevation {Math.round(mToFt(targetAltitude))}ft + + + (5) Target is {targetUnit ? targetUnit.getType() : "insert description"} + + + (6) 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} + + )} + + + (7) Marked by XXX + + + (8) Friendlies XXX + +
+
+ )} +
+
+ ); +} diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index 8239b759..d2ea04fb 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -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" > +
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) } }