mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
585 lines
25 KiB
TypeScript
585 lines
25 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import { Menu } from "./components/menu";
|
|
import { FaArrowDown, FaArrowUp, FaChevronRight, FaTrash } from "react-icons/fa";
|
|
import { getApp } from "../../olympusapp";
|
|
import { OlStateButton } from "../components/olstatebutton";
|
|
import { faDrawPolygon, faEye, faEyeSlash, faMapLocation } from "@fortawesome/free-solid-svg-icons";
|
|
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
|
import { CoalitionPolygon } from "../../map/coalitionarea/coalitionpolygon";
|
|
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
|
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
|
import { OlCheckbox } from "../components/olcheckbox";
|
|
import { Coalition } from "../../types/types";
|
|
import { OlRangeSlider } from "../components/olrangeslider";
|
|
import { CoalitionCircle } from "../../map/coalitionarea/coalitioncircle";
|
|
import { DrawSubState, ERAS_ORDER, IADSTypes, NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants";
|
|
import { AppStateChangedEvent, CoalitionAreasChangedEvent, CoalitionAreaSelectedEvent, DrawingsInitEvent, DrawingsUpdatedEvent } from "../../events";
|
|
import { FaCopy, FaPencil, FaRegCompass, FaXmark } from "react-icons/fa6";
|
|
import { deepCopyTable } from "../../other/utils";
|
|
import { DCSDrawing, DCSDrawingsContainer, DCSEmptyLayer } from "../../map/drawings/drawingsmanager";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { OlSearchBar } from "../components/olsearchbar";
|
|
|
|
export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
|
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
|
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
|
|
const [activeCoalitionArea, setActiveCoalitionArea] = useState(null as null | CoalitionPolygon | CoalitionCircle);
|
|
const [coalitionAreas, setCoalitionAreas] = useState([] as (CoalitionPolygon | CoalitionCircle)[]);
|
|
const [IADSDensity, setIADSDensity] = useState(50);
|
|
const [IADSDistribution, setIADSDistribution] = useState(50);
|
|
const [forceCoalitionAppropriateUnits, setForceCoalitionApproriateUnits] = useState(false);
|
|
|
|
const [typesSelection, setTypesSelection] = useState({});
|
|
const [erasSelection, setErasSelection] = useState({});
|
|
const [rangesSelection, setRangesSelection] = useState({});
|
|
|
|
const [openContainers, setOpenContainers] = useState([] as DCSDrawingsContainer[]);
|
|
const [mainDrawingsContainer, setDrawingsContainer] = useState({ container: null } as { container: null | DCSDrawingsContainer });
|
|
const [navpointsContainer, setNavpointsContainer] = useState({ container: null } as { container: null | DCSDrawingsContainer });
|
|
const [searchString, setSearchString] = useState("");
|
|
const [navpointSearchString, setNavpointSearchString] = useState("");
|
|
|
|
useEffect(() => {
|
|
AppStateChangedEvent.on((state, subState) => {
|
|
setAppState(state);
|
|
setAppSubState(subState);
|
|
});
|
|
DrawingsInitEvent.on((drawingContainer, navpointsContainer) => {
|
|
setDrawingsContainer({ container: drawingContainer });
|
|
setNavpointsContainer({ container: navpointsContainer });
|
|
});
|
|
DrawingsUpdatedEvent.on(() => {
|
|
setDrawingsContainer({ container: getApp().getDrawingsManager().getDrawingsContainer() });
|
|
setNavpointsContainer({ container: getApp().getDrawingsManager().getNavpointsContainer() });
|
|
});
|
|
}, []);
|
|
|
|
/* Get all the unique types and eras for groundunits */
|
|
let types = IADSTypes;
|
|
let eras = getApp()
|
|
?.getUnitsManager()
|
|
.getDatabase()
|
|
.getEras()
|
|
.sort((era1, era2) => (ERAS_ORDER[era1] > ERAS_ORDER[era2] ? 1 : -1));
|
|
|
|
useEffect(() => {
|
|
CoalitionAreaSelectedEvent.on((coalitionArea) => setActiveCoalitionArea(coalitionArea));
|
|
CoalitionAreasChangedEvent.on((coalitionAreas) => setCoalitionAreas([...coalitionAreas]));
|
|
}, []);
|
|
|
|
function getDrawingLabelColor(drawing: DCSDrawingsContainer | DCSDrawing) {
|
|
return drawing.getVisibility() ? `text-gray-200` : `text-gray-600`;
|
|
}
|
|
|
|
function renderDrawingsContainerControls(container: DCSDrawingsContainer, containerSearchString: string) {
|
|
if (container.hasSearchString(containerSearchString)) {
|
|
/* The following snippet automatically open containers that contains searched drawings */
|
|
if (!openContainers.includes(container) && containerSearchString != "") {
|
|
openContainers.push(container);
|
|
setOpenContainers([...openContainers]);
|
|
}
|
|
|
|
return (
|
|
<div className="ml-2 flex flex-col gap-2" key={container.getGuid()}>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex justify-between gap-2">
|
|
<FaChevronRight
|
|
className={`
|
|
my-auto
|
|
${openContainers.includes(container) && `rotate-90`}
|
|
cursor-pointer text-gray-400 transition-transform
|
|
`}
|
|
onClick={() => {
|
|
if (openContainers.includes(container)) {
|
|
let index = openContainers.indexOf(container);
|
|
openContainers.splice(index, 1);
|
|
} else {
|
|
openContainers.push(container);
|
|
}
|
|
setOpenContainers([...openContainers]);
|
|
}}
|
|
></FaChevronRight>
|
|
<FontAwesomeIcon
|
|
icon={container.getVisibility() ? faEye : faEyeSlash}
|
|
className={`
|
|
my-auto w-6 cursor-pointer
|
|
${getDrawingLabelColor(container)}
|
|
transition-transform
|
|
hover:scale-125 hover:text-gray-50
|
|
`}
|
|
onClick={() => {
|
|
if (container === mainDrawingsContainer.container) {
|
|
getApp().getMap().setOption("showMissionDrawings", !getApp().getMap().getOptions().showMissionDrawings);
|
|
} else {
|
|
container.setVisibility(!container.getVisibility(), true);
|
|
}
|
|
}}
|
|
/>
|
|
<div
|
|
className={`
|
|
w-40
|
|
${getDrawingLabelColor(container)}
|
|
w-max-40 overflow-hidden text-ellipsis text-nowrap bg-
|
|
`}
|
|
>
|
|
{container.getName()}
|
|
</div>
|
|
|
|
<OlRangeSlider
|
|
value={container.getOpacity() * 100}
|
|
min={0}
|
|
max={100}
|
|
onChange={(ev) => {
|
|
container.setOpacity(Number(ev.currentTarget.value) / 100);
|
|
}}
|
|
className={`my-auto ml-auto max-w-32`}
|
|
></OlRangeSlider>
|
|
</div>
|
|
</div>
|
|
{openContainers.includes(container) &&
|
|
container.getSubContainers().map((container) => renderDrawingsContainerControls(container, containerSearchString))}
|
|
{openContainers.includes(container) &&
|
|
container.getDrawings().map((drawing, index) => {
|
|
if (drawing instanceof DCSEmptyLayer) return <></>;
|
|
if (!drawing.getName().toLowerCase().includes(containerSearchString.toLowerCase())) return <></>;
|
|
return (
|
|
<div className="ml-4 flex justify-start gap-2" key={index}>
|
|
<FontAwesomeIcon
|
|
icon={drawing.getVisibility() ? faEye : faEyeSlash}
|
|
className={`
|
|
my-auto w-6 cursor-pointer
|
|
${getDrawingLabelColor(drawing)}
|
|
transition-transform
|
|
hover:scale-125 hover:text-gray-50
|
|
`}
|
|
onClick={() => {
|
|
drawing.setVisibility(!drawing.getVisibility());
|
|
}}
|
|
/>
|
|
<div className={`
|
|
overflow-hidden
|
|
${getDrawingLabelColor(drawing)}
|
|
text-ellipsis text-nowrap
|
|
`}>{drawing.getName()}</div>
|
|
<FontAwesomeIcon
|
|
icon={faMapLocation}
|
|
className={`
|
|
ml-auto cusor-pointer transition-transform
|
|
hover:scale-125
|
|
`}
|
|
onClick={() => {
|
|
const latLng = drawing.getLayer()["getLatLng"] && drawing.getLayer()["getLatLng"]();
|
|
const bounds = drawing.getLayer()["getBounds"] && drawing.getLayer()["getBounds"]();
|
|
latLng && getApp().getMap().setView(latLng, 14);
|
|
bounds && getApp().getMap().fitBounds(bounds);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
} else {
|
|
return <></>;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Menu
|
|
open={props.open}
|
|
title="Draw"
|
|
onClose={props.onClose}
|
|
showBackButton={appSubState !== DrawSubState.NO_SUBSTATE}
|
|
onBack={() => {
|
|
getApp().getCoalitionAreasManager().setSelectedArea(null);
|
|
getApp().setState(OlympusState.DRAW, DrawSubState.NO_SUBSTATE);
|
|
}}
|
|
wiki={() => {
|
|
return (
|
|
<div
|
|
className={`
|
|
h-full flex-col overflow-auto p-4 text-gray-400 no-scrollbar flex
|
|
gap-2
|
|
`}
|
|
>
|
|
<h2 className="mb-4 font-bold">Drawing menu</h2>
|
|
<div>
|
|
The drawing menu allows you to create and manage custom drawings, such as polygons and circles, and to generate IADS (Integrated Air Defense
|
|
System) areas. Moreover, you can manage the visibility and opacity of mission drawings, i.e. drawings from the Mission Editor.
|
|
</div>
|
|
<h2 className="my-4 font-bold">Custom drawings and IADS</h2>
|
|
<div>
|
|
To create a custom drawing, click on the 'Add polygon' or 'Add circle' buttons, then click on the map to add polygons or to move the drawing.
|
|
Double-click on the map to finish your creation. You can then edit the drawing by clicking on it. You can also move it up or down in the list, or
|
|
delete it.
|
|
</div>
|
|
<div>
|
|
You can change the name and the coalition of the area. You can also generate an IADS area by selecting the types, eras, and ranges of units you
|
|
want to include in the area. You can also set the density and distribution of the IADS. If you check the 'Force coalition appropriate units' box,
|
|
the IADS will only include units that are appropriate for the coalition of the area (e.g. Hawk SAMs for {""}
|
|
<span className="text-blue-500">blue</span> and SA-6 SAMs for <span className={`
|
|
text-red-500
|
|
`}>red</span>
|
|
).
|
|
</div>
|
|
<div>
|
|
The IADS generator will create a random distribution of units in the area, based on the density and distribution you set. Units will be
|
|
concentrated around cities, and airbases that belong to the selected coalition.
|
|
</div>
|
|
<h2 className="my-4 font-bold">Mission drawings</h2>
|
|
<div>
|
|
You can manage the visibility and opacity of mission drawings by clicking on the eye icon. Moreover, you can change the opacity of the drawing by
|
|
using the slider. You can also hide or show all the drawings in a container.
|
|
</div>
|
|
<div>
|
|
You can search for a specific drawing by typing in the search bar. The search is case-insensitive and will match any part of the drawing name.
|
|
</div>
|
|
<div>Any change you make is persistent and will be saved for the next time you reload Olympus, as long as the DCS mission was not restarted.</div>
|
|
</div>
|
|
);
|
|
}}
|
|
>
|
|
<>
|
|
{appState === OlympusState.DRAW && appSubState === DrawSubState.NO_SUBSTATE && (
|
|
<div className="flex flex-col gap-2 text-sm text-gray-400">
|
|
<div className="flex flex-col bg-olympus-200/30">
|
|
{coalitionAreas.map((coalitionArea) => {
|
|
return (
|
|
<div
|
|
data-coalition={coalitionArea.getCoalition()}
|
|
className={`
|
|
flex cursor-pointer content-center border-l-4 px-4
|
|
text-base text-white
|
|
data-[coalition="blue"]:border-blue-500
|
|
data-[coalition="neutral"]:border-gray-500
|
|
data-[coalition="red"]:border-red-500
|
|
hover:bg-white/10
|
|
`}
|
|
onClick={() => {
|
|
coalitionArea.setSelected(true);
|
|
getApp().setState(OlympusState.DRAW, DrawSubState.EDIT);
|
|
}}
|
|
>
|
|
<div className="py-3">{coalitionArea.getLabelText()}</div>
|
|
<FaArrowUp
|
|
onClick={(ev) => {
|
|
ev.stopPropagation();
|
|
getApp().getCoalitionAreasManager().moveAreaUp(coalitionArea);
|
|
}}
|
|
className={`
|
|
my-auto ml-auto rounded-md p-2 text-3xl
|
|
hover:bg-white/10
|
|
`}
|
|
/>
|
|
<FaArrowDown
|
|
onClick={(ev) => {
|
|
ev.stopPropagation();
|
|
getApp().getCoalitionAreasManager().moveCoalitionAreaDown(coalitionArea);
|
|
}}
|
|
className={`
|
|
my-auto rounded-md p-2 text-3xl
|
|
hover:bg-white/10
|
|
`}
|
|
/>
|
|
<FaXmark
|
|
onClick={(ev) => {
|
|
ev.stopPropagation();
|
|
getApp().getCoalitionAreasManager().deleteCoalitionArea(coalitionArea);
|
|
}}
|
|
className={`
|
|
my-auto rounded-md p-2 text-3xl
|
|
hover:bg-red-500/50
|
|
`}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2 p-6">
|
|
<OlStateButton
|
|
className="!w-full"
|
|
icon={faDrawPolygon}
|
|
checked={false}
|
|
onClick={() => getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_POLYGON)}
|
|
>
|
|
<div className="text-sm">Add polygon</div>
|
|
</OlStateButton>
|
|
<OlStateButton className="!w-full" icon={faCircle} checked={false} onClick={() => getApp().setState(OlympusState.DRAW, DrawSubState.DRAW_CIRCLE)}>
|
|
<div className="text-sm">Add circle</div>
|
|
</OlStateButton>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex flex-col gap-2 p-6">
|
|
<div
|
|
className={`flex flex-row items-center text-sm text-gray-400`}
|
|
>
|
|
<span
|
|
className={`
|
|
mr-2 px-1 py-1 text-center font-bold text-olympus-700
|
|
text-white
|
|
`}
|
|
>
|
|
<FaPencil />
|
|
</span>
|
|
Mission drawings
|
|
</div>
|
|
<OlSearchBar
|
|
key="main-search"
|
|
onChange={(search) => {
|
|
setSearchString(search);
|
|
if (search === "") {
|
|
setOpenContainers([]);
|
|
}
|
|
}}
|
|
text={searchString || ""}
|
|
></OlSearchBar>
|
|
<div className="flex flex-col gap-2">
|
|
{mainDrawingsContainer.container && renderDrawingsContainerControls(mainDrawingsContainer.container, searchString)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2 p-6">
|
|
<div
|
|
className={`flex flex-row items-center text-sm text-gray-400`}
|
|
>
|
|
<span
|
|
className={`
|
|
mr-2 px-1 py-1 text-center font-bold text-olympus-700
|
|
text-white
|
|
`}
|
|
>
|
|
<FaRegCompass />
|
|
</span>
|
|
Navpoints
|
|
</div>
|
|
<OlSearchBar
|
|
key="navpoint-search"
|
|
onChange={(search) => {
|
|
setNavpointSearchString(search);
|
|
if (search === "") {
|
|
setOpenContainers([]);
|
|
}
|
|
}}
|
|
text={navpointSearchString || ""}
|
|
></OlSearchBar>
|
|
<div className="flex flex-col gap-2">
|
|
{navpointsContainer.container && renderDrawingsContainerControls(navpointsContainer.container, navpointSearchString)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
<div>
|
|
{activeCoalitionArea !== null && (
|
|
<div className={`flex flex-col gap-4 py-4`}>
|
|
<div
|
|
className={`
|
|
flex flex-col content-center justify-start gap-2 px-6
|
|
text-gray-200
|
|
`}
|
|
>
|
|
<div className="my-auto flex justify-between text-md">
|
|
Area label
|
|
<div
|
|
className={`
|
|
cursor-pointer rounded-md bg-red-600 p-2
|
|
hover:bg-red-400
|
|
`}
|
|
onClick={() => {
|
|
getApp().getCoalitionAreasManager().deleteCoalitionArea(activeCoalitionArea);
|
|
getApp().setState(OlympusState.DRAW);
|
|
setActiveCoalitionArea(null);
|
|
}}
|
|
>
|
|
<FaTrash className={`text-gray-50`}></FaTrash>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
className={`
|
|
block w-full flex-grow rounded-lg border border-gray-300
|
|
bg-gray-50 p-2.5 text-sm text-gray-900
|
|
dark:border-gray-600 dark:bg-gray-700 dark:text-white
|
|
dark:placeholder-gray-400 dark:focus:border-blue-500
|
|
dark:focus:ring-blue-500
|
|
focus:border-blue-500 focus:ring-blue-500
|
|
`}
|
|
placeholder={activeCoalitionArea.getLabelText()}
|
|
onInput={(ev) => activeCoalitionArea.setLabelText(ev.currentTarget.value)}
|
|
></input>
|
|
</div>
|
|
<div
|
|
className={`
|
|
flex content-center justify-start gap-4 px-6 text-gray-200
|
|
`}
|
|
>
|
|
<div className="my-auto text-md">Coalition: </div>
|
|
<OlCoalitionToggle
|
|
coalition={activeCoalitionArea.getCoalition()}
|
|
onClick={() => {
|
|
let newCoalition = "";
|
|
if (activeCoalitionArea.getCoalition() === "blue") newCoalition = "neutral";
|
|
else if (activeCoalitionArea.getCoalition() === "neutral") newCoalition = "red";
|
|
else if (activeCoalitionArea.getCoalition() === "red") newCoalition = "blue";
|
|
activeCoalitionArea.setCoalition(newCoalition as Coalition);
|
|
}}
|
|
></OlCoalitionToggle>
|
|
</div>
|
|
<div
|
|
className={`
|
|
flex flex-col gap-3 border-l-4 border-l-olympus-100
|
|
bg-olympus-600 p-5
|
|
`}
|
|
>
|
|
<div
|
|
className={`border-b-2 border-b-olympus-100 pb-4 text-gray-300`}
|
|
>
|
|
Automatic IADS generation
|
|
</div>
|
|
<OlDropdown className="" label="Units types" disableAutoClose={true}>
|
|
{types.map((type, idx) => {
|
|
if (!(type in typesSelection)) {
|
|
typesSelection[type] = true;
|
|
setTypesSelection(deepCopyTable(typesSelection));
|
|
}
|
|
|
|
return (
|
|
<OlDropdownItem key={idx} className={`flex gap-4`}>
|
|
<OlCheckbox
|
|
checked={typesSelection[type]}
|
|
onChange={(ev) => {
|
|
typesSelection[type] = ev.currentTarget.checked;
|
|
setTypesSelection(deepCopyTable(typesSelection));
|
|
}}
|
|
/>
|
|
<div>{type}</div>
|
|
</OlDropdownItem>
|
|
);
|
|
})}
|
|
</OlDropdown>
|
|
<OlDropdown className="" label="Units eras" disableAutoClose={true}>
|
|
{eras.map((era) => {
|
|
if (!(era in erasSelection)) {
|
|
erasSelection[era] = true;
|
|
setErasSelection(deepCopyTable(erasSelection));
|
|
}
|
|
|
|
return (
|
|
<OlDropdownItem className={`flex gap-4`}>
|
|
<OlCheckbox
|
|
checked={erasSelection[era]}
|
|
onChange={(ev) => {
|
|
erasSelection[era] = ev.currentTarget.checked;
|
|
setErasSelection(deepCopyTable(erasSelection));
|
|
}}
|
|
/>
|
|
<div>{era}</div>
|
|
</OlDropdownItem>
|
|
);
|
|
})}
|
|
</OlDropdown>
|
|
<OlDropdown className="" label="Units ranges" disableAutoClose={true}>
|
|
{["Short range", "Medium range", "Long range"].map((range) => {
|
|
if (!(range in rangesSelection)) {
|
|
rangesSelection[range] = true;
|
|
setRangesSelection(deepCopyTable(rangesSelection));
|
|
}
|
|
|
|
return (
|
|
<OlDropdownItem className={`flex gap-4`}>
|
|
<OlCheckbox
|
|
checked={rangesSelection[range]}
|
|
onChange={(ev) => {
|
|
rangesSelection[range] = ev.currentTarget.checked;
|
|
setErasSelection(deepCopyTable(rangesSelection));
|
|
}}
|
|
/>
|
|
<div>{range}</div>
|
|
</OlDropdownItem>
|
|
);
|
|
})}
|
|
</OlDropdown>
|
|
<div>
|
|
<div className="flex justify-between">
|
|
<div className="text-gray-100">IADS Density</div>
|
|
<div
|
|
className={`
|
|
font-bold
|
|
dark:text-blue-500
|
|
`}
|
|
>
|
|
{IADSDensity}%
|
|
</div>
|
|
</div>
|
|
<OlRangeSlider
|
|
value={IADSDensity}
|
|
onChange={(ev) => {
|
|
setIADSDensity(Number(ev.currentTarget.value));
|
|
}}
|
|
></OlRangeSlider>
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between">
|
|
<div className="text-gray-100">IADS Distribution</div>
|
|
<div
|
|
className={`
|
|
font-bold
|
|
dark:text-blue-500
|
|
`}
|
|
>
|
|
{IADSDistribution}%
|
|
</div>
|
|
</div>
|
|
<OlRangeSlider
|
|
value={IADSDistribution}
|
|
onChange={(ev) => {
|
|
setIADSDistribution(Number(ev.target.value));
|
|
}}
|
|
></OlRangeSlider>
|
|
</div>
|
|
<div className="flex content-center gap-4 text-gray-200">
|
|
<OlCheckbox
|
|
checked={forceCoalitionAppropriateUnits}
|
|
onChange={() => {
|
|
setForceCoalitionApproriateUnits(!forceCoalitionAppropriateUnits);
|
|
}}
|
|
/>
|
|
Force coalition appropriate units
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className={`
|
|
mb-2 me-2 rounded-lg bg-blue-700 px-5 py-2.5 text-sm
|
|
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
|
|
`}
|
|
onClick={() =>
|
|
getApp()
|
|
.getUnitsManager()
|
|
.createIADS(
|
|
activeCoalitionArea,
|
|
typesSelection,
|
|
erasSelection,
|
|
rangesSelection,
|
|
IADSDensity,
|
|
IADSDistribution,
|
|
forceCoalitionAppropriateUnits
|
|
)
|
|
}
|
|
>
|
|
Generate IADS
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Menu>
|
|
);
|
|
}
|