From ebfa7916c6839e5e07899cd8ceb7f71dc9da1475 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 29 Aug 2024 15:31:16 +0200 Subject: [PATCH] Added airbase info and spawn menu --- frontend/react/src/eventscontext.tsx | 2 + frontend/react/src/mission/airbase.ts | 10 +- frontend/react/src/mission/missionmanager.ts | 8 +- frontend/react/src/server/servermanager.ts | 2 - frontend/react/src/statecontext.tsx | 1 + .../react/src/ui/components/olaccordion.tsx | 3 +- frontend/react/src/ui/panels/airbasemenu.tsx | 141 ++++++++++++++++++ .../{controls.tsx => controlspanel.tsx} | 0 frontend/react/src/ui/panels/sidebar.tsx | 8 +- frontend/react/src/ui/panels/spawnmenu.tsx | 6 +- .../react/src/ui/panels/unitspawnmenu.tsx | 128 +++++++++++----- frontend/react/src/ui/ui.tsx | 24 ++- 12 files changed, 272 insertions(+), 61 deletions(-) create mode 100644 frontend/react/src/ui/panels/airbasemenu.tsx rename frontend/react/src/ui/panels/{controls.tsx => controlspanel.tsx} (100%) diff --git a/frontend/react/src/eventscontext.tsx b/frontend/react/src/eventscontext.tsx index 52453b28..fdf399d8 100644 --- a/frontend/react/src/eventscontext.tsx +++ b/frontend/react/src/eventscontext.tsx @@ -7,12 +7,14 @@ export const EventsContext = createContext({ setMeasureMenuVisible: (e: boolean) => {}, setDrawingMenuVisible: (e: boolean) => {}, setOptionsMenuVisible: (e: boolean) => {}, + setAirbaseMenuVisible: (e: boolean) => {}, toggleMainMenuVisible: () => {}, toggleSpawnMenuVisible: () => {}, toggleUnitControlMenuVisible: () => {}, toggleMeasureMenuVisible: () => {}, toggleDrawingMenuVisible: () => {}, toggleOptionsMenuVisible: () => {}, + toggleAirbaseMenuVisible: () => {}, }); export const EventsProvider = EventsContext.Provider; diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts index 5894558f..0c214126 100644 --- a/frontend/react/src/mission/airbase.ts +++ b/frontend/react/src/mission/airbase.ts @@ -12,7 +12,6 @@ export class Airbase extends CustomMarker { runways: [], }; #coalition: string = ""; - #hasChartDataBeenSet: boolean = false; #properties: string[] = []; #parkings: string[] = []; @@ -22,10 +21,6 @@ export class Airbase extends CustomMarker { this.#name = options.name; } - chartDataHasBeenSet() { - return this.#hasChartDataBeenSet; - } - createIcon() { var icon = new DivIcon({ className: "leaflet-airbase-marker", @@ -43,10 +38,10 @@ export class Airbase extends CustomMarker { el.appendChild(img); this.getElement()?.appendChild(el); el.addEventListener("mouseover", (ev) => { - document.dispatchEvent(new CustomEvent("airbaseMouseover", { detail: this })); + document.dispatchEvent(new CustomEvent("airbasemouseover", { detail: this })); }); el.addEventListener("mouseout", (ev) => { - document.dispatchEvent(new CustomEvent("airbaseMouseout", { detail: this })); + document.dispatchEvent(new CustomEvent("airbasemouseout", { detail: this })); }); el.dataset.coalition = this.#coalition; } @@ -73,7 +68,6 @@ export class Airbase extends CustomMarker { } setChartData(chartData: AirbaseChartData) { - this.#hasChartDataBeenSet = true; this.#chartData = chartData; } diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 68653367..f36b23d6 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -87,7 +87,7 @@ export class MissionManager { position: new LatLng(airbase.latitude, airbase.longitude), name: airbase.callsign, }).addTo(getApp().getMap()); - this.#airbases[airbase.callsign].on("contextmenu", (e) => this.#onAirbaseClick(e)); + this.#airbases[airbase.callsign].on("click", (e) => this.#onAirbaseClick(e)); this.#loadAirbaseChartData(airbase.callsign); } @@ -316,7 +316,9 @@ export class MissionManager { if (requestRefresh) getApp().getServerManager().refreshAll(); } - #onAirbaseClick(e: any) {} + #onAirbaseClick(ev: any) { + document.dispatchEvent(new CustomEvent("airbaseclick", { detail: ev.target })); + } #loadAirbaseChartData(callsign: string) { if (!this.#theatre) { @@ -324,7 +326,7 @@ export class MissionManager { } var xhr = new XMLHttpRequest(); - xhr.open("GET", `api/airbases/${this.#theatre.toLowerCase()}/${callsign}`, true); + xhr.open("GET", window.location.href.split("?")[0].replace("vite/", "") + `api/airbases/${this.#theatre.toLowerCase()}/${callsign}`, true); xhr.responseType = "json"; xhr.onload = () => { var status = xhr.status; diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index d6779888..108a52ea 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -142,8 +142,6 @@ export class ServerManager { setAddress(address: string) { this.#REST_ADDRESS = `${address.replace("vite/", "").replace("vite", "")}olympus`; - // TODO: TEMPORARY FOR DEBUGGING - // this.#REST_ADDRESS = `https://refugees.dcsolympus.com/olympus`; console.log(`Setting REST address to ${this.#REST_ADDRESS}`); } diff --git a/frontend/react/src/statecontext.tsx b/frontend/react/src/statecontext.tsx index 9281bf2f..5c2d9577 100644 --- a/frontend/react/src/statecontext.tsx +++ b/frontend/react/src/statecontext.tsx @@ -8,6 +8,7 @@ export const StateContext = createContext({ measureMenuVisible: false, drawingMenuVisible: false, optionsMenuVisible: false, + airbaseMenuVisible: false, mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS, mapOptions: MAP_OPTIONS_DEFAULTS, mapSources: [] as string[], diff --git a/frontend/react/src/ui/components/olaccordion.tsx b/frontend/react/src/ui/components/olaccordion.tsx index eda36fb6..b855ce90 100644 --- a/frontend/react/src/ui/components/olaccordion.tsx +++ b/frontend/react/src/ui/components/olaccordion.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowCircleDown } from "@fortawesome/free-solid-svg-icons"; -export function OlAccordion(props: { title: string; children?: JSX.Element | JSX.Element[]; showArrows?: boolean }) { +export function OlAccordion(props: { title: string; children?: JSX.Element | JSX.Element[]; showArrows?: boolean; className?: string }) { const [open, setOpen] = useState(false); const [scrolledUp, setScrolledUp] = useState(true); const [scrolledDown, setScrolledDown] = useState(false); @@ -31,6 +31,7 @@ export function OlAccordion(props: { title: string; children?: JSX.Element | JSX type="button" onClick={() => setOpen(!open)} className={` + ${props.className ?? ""} flex w-full items-center justify-between gap-3 border-gray-200 py-2 text-gray-700 dark:border-gray-700 dark:text-white diff --git a/frontend/react/src/ui/panels/airbasemenu.tsx b/frontend/react/src/ui/panels/airbasemenu.tsx new file mode 100644 index 00000000..05224ebf --- /dev/null +++ b/frontend/react/src/ui/panels/airbasemenu.tsx @@ -0,0 +1,141 @@ +import React, { useState } from "react"; +import { Menu } from "./components/menu"; +import { OlCheckbox } from "../components/olcheckbox"; +import { OlRangeSlider } from "../components/olrangeslider"; +import { OlNumberInput } from "../components/olnumberinput"; +import { Coalition, MapOptions } from "../../types/types"; +import { getApp } from "../../olympusapp"; +import { Airbase } from "../../mission/airbase"; +import { FaArrowLeft, FaCompass } from "react-icons/fa6"; +import { getUnitsByLabel } from "../../other/utils"; +import { UnitBlueprint } from "../../interfaces"; +import { IDLE } from "../../constants/constants"; +import { OlSearchBar } from "../components/olsearchbar"; +import { OlAccordion } from "../components/olaccordion"; +import { OlUnitEntryList } from "../components/olunitlistentry"; +import { olButtonsVisibilityAircraft, olButtonsVisibilityHelicopter } from "../components/olicons"; +import { UnitSpawnMenu } from "./unitspawnmenu"; + +export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase: Airbase | null; children?: JSX.Element | JSX.Element[] }) { + const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint); + const [filterString, setFilterString] = useState(""); + + const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = getUnitsByLabel(filterString); + + return ( + +
+
+
+ ICAO name + {props.airbase?.getChartData().ICAO !== "" ? props.airbase?.getChartData().ICAO : "N/A"} +
+
+ TACAN + {props.airbase?.getChartData().TACAN !== "" ? props.airbase?.getChartData().TACAN : "None"} +
+
+ Elevation + {props.airbase?.getChartData().elevation !== "" ? props.airbase?.getChartData().elevation : "N/A"}ft +
+ + +
+ {props.airbase?.getChartData().runways.map((runway) => { + return ( + <> + {Object.keys(runway.headings[0]).map((runwayName) => { + return ( +
+ + {" "} + RWY {runwayName} + + + {runway.headings[0][runwayName].magHeading}°{" "} + ILS{" "} + {runway.headings[0][runwayName].ILS !== "" ? runway.headings[0][runwayName].ILS + "MHz" : "None"} + +
+ ); + })} + + ); + })} +
+
+
+ +
+ {blueprint && ( + setBlueprint(null)} + /> + )} + Spawn units at airbase +
+ <> + {blueprint === null && ( +
+ setFilterString(value)} text={filterString} /> + +
+ {Object.entries(filteredAircraft).map((entry) => { + return setBlueprint(entry[1])} />; + })} +
+
+ +
+ {Object.entries(filteredHelicopters).map((entry) => { + return setBlueprint(entry[1])} />; + })} +
+
+
+ )} + + {!(blueprint === null) && ( + + )} + +
+
+ ); +} diff --git a/frontend/react/src/ui/panels/controls.tsx b/frontend/react/src/ui/panels/controlspanel.tsx similarity index 100% rename from frontend/react/src/ui/panels/controls.tsx rename to frontend/react/src/ui/panels/controlspanel.tsx diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index 58e856ca..a4642cf7 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { OlStateButton } from "../components/olstatebutton"; -import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faPlaneDeparture } from "@fortawesome/free-solid-svg-icons"; import { EventsConsumer } from "../../eventscontext"; import { StateConsumer } from "../../statecontext"; import { IDLE } from "../../constants/constants"; @@ -52,6 +52,12 @@ export function SideBar() { icon={faPencil} tooltip="Hide/show drawing menu" > +
diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index a08cd847..7df3b1ca 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -1,7 +1,5 @@ import React, { useState, useEffect } from "react"; import { Menu } from "./components/menu"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { library } from "@fortawesome/fontawesome-svg-core"; import { OlSearchBar } from "../components/olsearchbar"; import { OlAccordion } from "../components/olaccordion"; import { getApp } from "../../olympusapp"; @@ -18,8 +16,6 @@ import { import { IDLE, SPAWN_UNIT } from "../../constants/constants"; import { getUnitsByLabel } from "../../other/utils"; -library.add(faPlus); - export function SpawnMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint); const [filterString, setFilterString] = useState(""); @@ -107,7 +103,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
)} - {!(blueprint === null) && } + {!(blueprint === null) && } ); diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 318d47ad..65c91273 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -11,8 +11,9 @@ import { getApp } from "../../olympusapp"; import { IDLE, SPAWN_UNIT } from "../../constants/constants"; import { ftToM, getUnitCategoryByBlueprint } from "../../other/utils"; import { LatLng } from "leaflet"; +import { Airbase } from "../../mission/airbase"; -export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) { +export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation: boolean; airbase?: Airbase | null; coalition?: Coalition }) { /* Compute the min and max values depending on the unit type */ const minNumber = 1; const maxNumber = 4; @@ -30,31 +31,60 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) { /* When the menu is opened show the unit preview on the map as a cursor */ useEffect(() => { - if (props.blueprint !== null) { - getApp() - ?.getMap() - ?.setState(SPAWN_UNIT, { - spawnRequestTable: { - category: getUnitCategoryByBlueprint(props.blueprint), - unit: { - unitType: props.blueprint.name, - location: new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit - skill: "High", - liveryID: "", - altitude: ftToM(spawnAltitude), - loadout: - props.blueprint.loadouts?.find((loadout) => { - return loadout.name === spawnLoadoutName; - })?.code ?? "", + if (props.coalition && props.coalition !== spawnCoalition) { + setSpawnCoalition(props.coalition); + } + if (props.spawnAtLocation) { + if (props.blueprint !== null) { + getApp() + ?.getMap() + ?.setState(SPAWN_UNIT, { + spawnRequestTable: { + category: getUnitCategoryByBlueprint(props.blueprint), + unit: { + unitType: props.blueprint.name, + location: new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit + skill: "High", + liveryID: "", + altitude: ftToM(spawnAltitude), + loadout: + props.blueprint.loadouts?.find((loadout) => { + return loadout.name === spawnLoadoutName; + })?.code ?? "", + }, + coalition: spawnCoalition, }, - coalition: spawnCoalition, - }, - }); - } else { - if (getApp()?.getMap()?.getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE); + }); + } else { + if (getApp()?.getMap()?.getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE); + } } }); + function spawnAtAirbase() { + getApp() + .getUnitsManager() + .spawnUnits( + getUnitCategoryByBlueprint(props.blueprint), + [ + { + unitType: props.blueprint.name, + location: new LatLng(0, 0), // Not relevant spawning at airbase + skill: "High", + liveryID: "", + altitude: 0, + loadout: + props.blueprint.loadouts?.find((loadout) => { + return loadout.name === spawnLoadoutName; + })?.code ?? "", + }, + ], + props.coalition, + false, + props.airbase?.getName() + ); + } + /* Get a list of all the roles */ const roles: string[] = []; (props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => { @@ -87,14 +117,16 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) { inline-flex w-full flex-row content-center justify-between `} > - { - spawnCoalition === "blue" && setSpawnCoalition("neutral"); - spawnCoalition === "neutral" && setSpawnCoalition("red"); - spawnCoalition === "red" && setSpawnCoalition("blue"); - }} - /> + {!props.coalition && ( + { + spawnCoalition === "blue" && setSpawnCoalition("neutral"); + spawnCoalition === "neutral" && setSpawnCoalition("red"); + spawnCoalition === "red" && setSpawnCoalition("blue"); + }} + /> + )} -
- {spawnLoadout && - spawnLoadout.items.map((item) => { + {spawnLoadout && spawnLoadout.items.length > 0 && ( +
+ {spawnLoadout.items.map((item) => { return (
); })} -
+
+ )} + {!props.spawnAtLocation && ( + + )}
); } diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index c8af5f23..df013bc2 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -18,16 +18,19 @@ import { sha256 } from "js-sha256"; import { MiniMapPanel } from "./panels/minimappanel"; import { UnitMouseControlBar } from "./panels/unitmousecontrolbar"; import { DrawingMenu } from "./panels/drawingmenu"; -import { ControlsPanel } from "./panels/controls"; +import { ControlsPanel } from "./panels/controlspanel"; import { MapContextMenu } from "./contextmenus/mapcontextmenu"; +import { AirbaseMenu } from "./panels/airbasemenu"; +import { Airbase } from "../mission/airbase"; -export type OlympusState = { +export type OlympusUIState = { mainMenuVisible: boolean; spawnMenuVisible: boolean; unitControlMenuVisible: boolean; measureMenuVisible: boolean; drawingMenuVisible: boolean; optionsMenuVisible: boolean; + airbaseMenuVisible: boolean; mapHiddenTypes: MapHiddenTypes; mapOptions: MapOptions; }; @@ -40,6 +43,7 @@ export function UI() { const [measureMenuVisible, setMeasureMenuVisible] = useState(false); const [drawingMenuVisible, setDrawingMenuVisible] = useState(false); const [optionsMenuVisible, setOptionsMenuVisible] = useState(false); + const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false); const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS); const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS); const [checkingPassword, setCheckingPassword] = useState(false); @@ -48,6 +52,7 @@ export function UI() { const [mapSources, setMapSources] = useState([] as string[]); const [activeMapSource, setActiveMapSource] = useState(""); const [mapState, setMapState] = useState(IDLE); + const [airbase, setAirbase] = useState(null as null | Airbase); useEffect(() => { document.addEventListener("hiddenTypesChanged", (ev) => { @@ -75,6 +80,13 @@ export function UI() { setMapSources(sources); setActiveMapSource(sources[0]); }); + + document.addEventListener("airbaseclick", (ev) => { + hideAllMenus(); + getApp().getMap().setState(IDLE); + setAirbase((ev as CustomEvent).detail); + setAirbaseMenuVisible(true); + }); }, []); function hideAllMenus() { @@ -84,6 +96,7 @@ export function UI() { setMeasureMenuVisible(false); setDrawingMenuVisible(false); setOptionsMenuVisible(false); + setAirbaseMenuVisible(false); } function checkPassword(password: string) { @@ -139,6 +152,7 @@ export function UI() { measureMenuVisible: measureMenuVisible, drawingMenuVisible: drawingMenuVisible, optionsMenuVisible: optionsMenuVisible, + airbaseMenuVisible: airbaseMenuVisible, mapOptions: mapOptions, mapHiddenTypes: mapHiddenTypes, mapSources: mapSources, @@ -154,6 +168,7 @@ export function UI() { setDrawingMenuVisible: setDrawingMenuVisible, setMeasureMenuVisible: setMeasureMenuVisible, setOptionsMenuVisible: setOptionsMenuVisible, + setAirbaseMenuVisible: setAirbaseMenuVisible, toggleMainMenuVisible: () => { hideAllMenus(); setMainMenuVisible(!mainMenuVisible); @@ -178,6 +193,10 @@ export function UI() { hideAllMenus(); setOptionsMenuVisible(!optionsMenuVisible); }, + toggleAirbaseMenuVisible: () => { + hideAllMenus(); + setAirbaseMenuVisible(!airbaseMenuVisible); + }, }} >
@@ -211,6 +230,7 @@ export function UI() { setOptionsMenuVisible(false)} options={mapOptions} /> setUnitControlMenuVisible(false)} /> setDrawingMenuVisible(false)} /> + setAirbaseMenuVisible(false)} airbase={airbase}/>