From f565b9ee6e2311a67997a90520e5f726e35ed0a0 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 21 Oct 2025 17:34:20 +0200 Subject: [PATCH 1/2] Add type annotations and key conversions in Map class Improves type safety by adding explicit type annotations to method parameters and callback functions in the Map class. Updates key handling for object properties to ensure correct types, particularly when interacting with ContextActions, MapOptions, MapHiddenTypes, and destination preview markers. --- frontend/react/src/map/map.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index bb34eb8a..ed3cdacc 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -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) { 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(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) { + 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) => { From 2a9723b9327b6de64d499794d3ca005d127bc8b0 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Thu, 23 Oct 2025 18:06:29 +0200 Subject: [PATCH 2/2] Add image overlay import modal and menu option Introduces ImageOverlayModal for importing image overlays with user-specified corner coordinates. Adds a menu item to trigger the modal and integrates it into the main UI component. Also updates OlNumberInput to support an internalClassName prop for styling flexibility. --- frontend/react/src/constants/constants.ts | 1 + .../react/src/ui/components/olnumberinput.tsx | 6 +- .../react/src/ui/modals/imageoverlaymodal.tsx | 180 ++++++++++++++++++ frontend/react/src/ui/panels/mainmenu.tsx | 25 +++ frontend/react/src/ui/ui.tsx | 2 + 5 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 frontend/react/src/ui/modals/imageoverlaymodal.tsx diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 28de5ad2..71f1896b 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -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"; diff --git a/frontend/react/src/ui/components/olnumberinput.tsx b/frontend/react/src/ui/components/olnumberinput.tsx index 0f981a4f..d10d9594 100644 --- a/frontend/react/src/ui/components/olnumberinput.tsx +++ b/frontend/react/src/ui/components/olnumberinput.tsx @@ -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: { `} >
{ setHoverTimeout( diff --git a/frontend/react/src/ui/modals/imageoverlaymodal.tsx b/frontend/react/src/ui/modals/imageoverlaymodal.tsx new file mode 100644 index 00000000..9a353a7d --- /dev/null +++ b/frontend/react/src/ui/modals/imageoverlaymodal.tsx @@ -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 ( + +
+
+ + Import Image Overlay + + + Enter the corner coordinates of the image overlay to be imported. +
+
+
Corner 1 latitude
+
+ { + setBound1Lat(ev.target.value); + }} + /> +
+
Corner 1 longitude
+
+ { + setBound1Lon(ev.target.value); + }} + /> +
+
+
+
Corner 2 latitude
+
+ { + setBound2Lat(ev.target.value); + }} + /> +
+
Corner 2 longitude
+
+ { + setBound2Lon(ev.target.value); + }} + /> +
+
+
+ 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. +
+
+
+ +
+ + + +
+
+
+ ); +} diff --git a/frontend/react/src/ui/panels/mainmenu.tsx b/frontend/react/src/ui/panels/mainmenu.tsx index 92c5fa72..781d514b 100644 --- a/frontend/react/src/ui/panels/mainmenu.tsx +++ b/frontend/react/src/ui/panels/mainmenu.tsx @@ -142,6 +142,31 @@ export function MainMenu(props: { open: boolean; onClose: () => void; children?: />
+
{ + getApp().setState(OlympusState.IMPORT_IMAGE_OVERLAY); + }} + > + {/**/} + Import image overlay +
+ +
+
); diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 65c33705..858b5425 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -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() { + )}