DCSOlympus/frontend/react/src/ui/panels/drawingmenu.tsx

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>
);
}