Merge branch 'release-candidate' into weapon-wizard

This commit is contained in:
Pax1601 2025-10-25 15:17:18 +02:00
commit 94d0b4d10e
6 changed files with 226 additions and 14 deletions

View File

@ -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";

View File

@ -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) => {

View File

@ -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(

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

View File

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

View File

@ -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} />
</>
)}