mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Merge branch 'release-candidate' into weapon-wizard
This commit is contained in:
commit
94d0b4d10e
@ -360,6 +360,7 @@ export enum OlympusState {
|
||||
MEASURE = "Measure",
|
||||
TRAINING = "Training",
|
||||
ADMIN = "Admin",
|
||||
IMPORT_IMAGE_OVERLAY = "Import image overlay"
|
||||
}
|
||||
|
||||
export const NO_SUBSTATE = "No substate";
|
||||
|
||||
@ -443,7 +443,7 @@ export class Map extends L.Map {
|
||||
ctrlKey: false,
|
||||
});
|
||||
|
||||
for (let contextActionName in ContextActions) {
|
||||
for (const contextActionName of Object.keys(ContextActions) as Array<keyof typeof ContextActions>) {
|
||||
const contextAction = ContextActions[contextActionName] as ContextAction;
|
||||
if (contextAction.getOptions().code) {
|
||||
getApp()
|
||||
@ -631,13 +631,13 @@ export class Map extends L.Map {
|
||||
return this.#spawnHeading;
|
||||
}
|
||||
|
||||
addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable, quickAccessName: string) {
|
||||
addStarredSpawnRequestTable(key: string, spawnRequestTable: SpawnRequestTable, quickAccessName: string) {
|
||||
this.#starredSpawnRequestTables[key] = spawnRequestTable;
|
||||
this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName;
|
||||
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
|
||||
}
|
||||
|
||||
removeStarredSpawnRequestTable(key) {
|
||||
removeStarredSpawnRequestTable(key: string) {
|
||||
if (key in this.#starredSpawnRequestTables) delete this.#starredSpawnRequestTables[key];
|
||||
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
|
||||
}
|
||||
@ -678,7 +678,7 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
setHiddenType(key: string, value: boolean) {
|
||||
this.#hiddenTypes[key] = value;
|
||||
this.#hiddenTypes[key as keyof MapHiddenTypes] = value;
|
||||
HiddenTypesChangedEvent.dispatch(this.#hiddenTypes);
|
||||
}
|
||||
|
||||
@ -788,13 +788,13 @@ export class Map extends L.Map {
|
||||
return smokeMarker;
|
||||
}
|
||||
|
||||
setOption(key, value) {
|
||||
setOption<K extends keyof MapOptions>(key: K, value: MapOptions[K]) {
|
||||
this.#options[key] = value;
|
||||
MapOptionsChangedEvent.dispatch(this.#options, key);
|
||||
MapOptionsChangedEvent.dispatch(this.#options, key as keyof MapOptions);
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
this.#options = { ...options };
|
||||
setOptions(options: Partial<MapOptions>) {
|
||||
this.#options = { ...this.#options, ...options } as MapOptions;
|
||||
MapOptionsChangedEvent.dispatch(this.#options);
|
||||
}
|
||||
|
||||
@ -1071,7 +1071,7 @@ export class Map extends L.Map {
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
(hash) => {
|
||||
(hash: string) => {
|
||||
this.addTemporaryMarker(
|
||||
e.latlng,
|
||||
this.#spawnRequestTable?.unit.unitType ?? "unknown",
|
||||
@ -1239,7 +1239,7 @@ export class Map extends L.Map {
|
||||
this.#lastMouseCoordinates = e.latlng;
|
||||
|
||||
MouseMovedEvent.dispatch(e.latlng);
|
||||
getGroundElevation(e.latlng, (elevation) => {
|
||||
getGroundElevation(e.latlng, (elevation: number) => {
|
||||
MouseMovedEvent.dispatch(e.latlng, elevation);
|
||||
});
|
||||
|
||||
@ -1366,8 +1366,8 @@ export class Map extends L.Map {
|
||||
.filter((unit) => !unit.getHuman());
|
||||
|
||||
Object.keys(this.#destinationPreviewMarkers).forEach((ID) => {
|
||||
this.#destinationPreviewMarkers[ID].removeFrom(this);
|
||||
delete this.#destinationPreviewMarkers[ID];
|
||||
this.#destinationPreviewMarkers[parseInt(ID)].removeFrom(this);
|
||||
delete this.#destinationPreviewMarkers[parseInt(ID)];
|
||||
});
|
||||
|
||||
if (this.#keepRelativePositions) {
|
||||
@ -1385,7 +1385,7 @@ export class Map extends L.Map {
|
||||
#moveDestinationPreviewMarkers() {
|
||||
if (this.#keepRelativePositions) {
|
||||
Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#destinationRotationCenter, this.#destinationRotation)).forEach(([ID, latlng]) => {
|
||||
this.#destinationPreviewMarkers[ID]?.setLatLng(latlng);
|
||||
this.#destinationPreviewMarkers[parseInt(ID)]?.setLatLng(latlng);
|
||||
});
|
||||
} else {
|
||||
Object.values(this.#destinationPreviewMarkers).forEach((marker) => {
|
||||
|
||||
@ -8,6 +8,7 @@ export function OlNumberInput(props: {
|
||||
max: number;
|
||||
minLength?: number;
|
||||
className?: string;
|
||||
internalClassName?: string;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
@ -34,7 +35,10 @@ export function OlNumberInput(props: {
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className="relative flex max-w-[8rem] items-center"
|
||||
className={`
|
||||
relative flex max-w-[8rem] items-center
|
||||
${props.internalClassName ?? ""}
|
||||
`}
|
||||
ref={buttonRef}
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
|
||||
180
frontend/react/src/ui/modals/imageoverlaymodal.tsx
Normal file
180
frontend/react/src/ui/modals/imageoverlaymodal.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { NO_SUBSTATE, OlympusState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent } from "../../events";
|
||||
import { ImageOverlay, LatLng, LatLngBounds } from "leaflet";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { OlStringInput } from "../components/olstringinput";
|
||||
|
||||
export function ImageOverlayModal(props: { open: boolean }) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE);
|
||||
const [bound1Lat, setBound1Lat] = useState("0");
|
||||
const [bound1Lon, setBound1Lon] = useState("0");
|
||||
const [bound2Lat, setBound2Lat] = useState("0");
|
||||
const [bound2Lon, setBound2Lon] = useState("0");
|
||||
const [importData, setImportData] = useState("");
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((appState, appSubState) => {
|
||||
setAppState(appState);
|
||||
setAppSubState(appSubState);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (appState !== OlympusState.IMPORT_IMAGE_OVERLAY) return;
|
||||
|
||||
setImportData("");
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
|
||||
input.onchange = async (e) => {
|
||||
// @ts-ignore TODO
|
||||
var file = e.target?.files[0];
|
||||
var reader = new FileReader();
|
||||
// Read the file content as image data URL
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = (readerEvent) => {
|
||||
// @ts-ignore TODO
|
||||
var content = readerEvent.target.result;
|
||||
if (content) {
|
||||
setImportData(content as string);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
input.click();
|
||||
}, [appState, appSubState]);
|
||||
|
||||
return (
|
||||
<Modal open={props.open} size="sm">
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<div className={`flex flex-col justify-between gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Import Image Overlay
|
||||
</span>
|
||||
|
||||
<span className="text-gray-400">Enter the corner coordinates of the image overlay to be imported.</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-gray-300">Corner 1 latitude </div>
|
||||
<div>
|
||||
<OlStringInput
|
||||
value={String(bound1Lat)}
|
||||
onChange={(ev) => {
|
||||
setBound1Lat(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray-300">Corner 1 longitude </div>
|
||||
<div>
|
||||
<OlStringInput
|
||||
value={String(bound1Lon)}
|
||||
onChange={(ev) => {
|
||||
setBound1Lon(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-gray-300">Corner 2 latitude </div>
|
||||
<div>
|
||||
<OlStringInput
|
||||
value={String(bound2Lat)}
|
||||
onChange={(ev) => {
|
||||
setBound2Lat(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray-300">Corner 2 longitude </div>
|
||||
<div>
|
||||
<OlStringInput
|
||||
value={String(bound2Lon)}
|
||||
onChange={(ev) => {
|
||||
setBound2Lon(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`
|
||||
${(showWarning ? "text-red-500" : `
|
||||
text-gray-400
|
||||
`)}
|
||||
text-sm
|
||||
`}>
|
||||
Please enter valid latitude and longitude values in decimal degrees format (e.g. 37.7749, -122.4194). Latitude must be between -90 and 90, and longitude must be between -180 and 180.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (
|
||||
isNaN(Number(bound1Lat)) || Number(bound1Lat) < -90 || Number(bound1Lat) > 90 ||
|
||||
isNaN(Number(bound1Lon)) || Number(bound1Lon) < -180 || Number(bound1Lon) > 180 ||
|
||||
isNaN(Number(bound2Lat)) || Number(bound2Lat) < -90 || Number(bound2Lat) > 90 ||
|
||||
isNaN(Number(bound2Lon)) || Number(bound2Lon) < -180 || Number(bound2Lon) > 180
|
||||
) {
|
||||
setShowWarning(true)
|
||||
return;
|
||||
}
|
||||
setShowWarning(false)
|
||||
|
||||
const bounds = new LatLngBounds([
|
||||
[Number(bound1Lat), Number(bound1Lon)],
|
||||
[Number(bound2Lat), Number(bound2Lon)]
|
||||
]
|
||||
)
|
||||
|
||||
let overlay = new ImageOverlay(importData, bounds);
|
||||
overlay.addTo(getApp().getMap());
|
||||
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center
|
||||
gap-2 rounded-sm 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
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.IDLE)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2
|
||||
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5
|
||||
text-sm font-medium text-white
|
||||
dark:border-gray-600 dark:bg-gray-800
|
||||
dark:text-gray-400 dark:hover:bg-gray-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -142,6 +142,31 @@ export function MainMenu(props: { open: boolean; onClose: () => void; children?:
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex cursor-pointer select-none content-center gap-3
|
||||
rounded-md p-2
|
||||
dark:hover:bg-olympus-500
|
||||
hover:bg-gray-900/10
|
||||
`}
|
||||
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.IMPORT_IMAGE_OVERLAY);
|
||||
}}
|
||||
>
|
||||
{/*<FontAwesomeIcon icon={faFileImport} className="my-auto w-4 text-gray-800 dark:text-gray-500" />*/}
|
||||
Import image overlay
|
||||
<div className={`ml-auto flex items-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRightLong}
|
||||
className={`
|
||||
my-auto px-2 text-right text-gray-800 transition-transform
|
||||
dark:text-olympus-50
|
||||
group-hover:translate-x-2
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@ -31,6 +31,7 @@ import { ImportExportModal } from "./modals/importexportmodal";
|
||||
import { WarningModal } from "./modals/warningmodal";
|
||||
import { TrainingModal } from "./modals/trainingmodal";
|
||||
import { AdminModal } from "./modals/adminmodal";
|
||||
import { ImageOverlayModal } from "./modals/imageoverlaymodal";
|
||||
|
||||
export function UI() {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
@ -74,6 +75,7 @@ export function UI() {
|
||||
<WarningModal open={appState === OlympusState.WARNING} />
|
||||
<TrainingModal open={appState === OlympusState.TRAINING} />
|
||||
<AdminModal open={appState === OlympusState.ADMIN} />
|
||||
<ImageOverlayModal open={appState === OlympusState.IMPORT_IMAGE_OVERLAY} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user