diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index b8693d13..a6f4f9f3 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -892,3 +892,19 @@ export class WeaponsRefreshedEvent { if (DEBUG) console.log(`Event ${this.name} dispatched`); } } + +export class CoordinatesFreezeEvent { + static on(callback: () => void) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(); + } + ) + } + + static dispatch() { + document.dispatchEvent(new CustomEvent(this.name)); + if (DEBUG) console.log(`Event ${this.name} dispatched`); + } +} diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 48ec8bde..d146c1c9 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -49,6 +49,7 @@ import { ConfigLoadedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, + CoordinatesFreezeEvent, HiddenTypesChangedEvent, MapContextMenuRequestEvent, MapOptionsChangedEvent, @@ -509,7 +510,7 @@ export class Map extends L.Map { code: "ShiftLeft", altKey: false, ctrlKey: false, - }); + }) } setLayerName(layerName: string) { @@ -1031,6 +1032,7 @@ export class Map extends L.Map { } #onLeftShortClick(e: L.LeafletMouseEvent) { + CoordinatesFreezeEvent.dispatch(); if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) { this.#debounceTimeout = window.setTimeout(() => { if (!this.#isSelecting) { diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index d2b01d07..4b39e816 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -769,4 +769,8 @@ export function secondsToTimeString(seconds: number) { const secs = Math.floor(seconds % 60); return `${zeroPad(hours, 2)}:${zeroPad(minutes, 2)}:${zeroPad(secs, 2)}`; +} + +export function isTrustedEnvironment() { + return window.location.protocol === "https:"; } \ No newline at end of file diff --git a/frontend/react/src/ui/components/ollocation.tsx b/frontend/react/src/ui/components/ollocation.tsx index f6a76ef2..f5ab162a 100644 --- a/frontend/react/src/ui/components/ollocation.tsx +++ b/frontend/react/src/ui/components/ollocation.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { LatLng } from "leaflet"; import { ConvertDDToDMS, DDToDDM, latLngToMGRS, latLngToUTM, zeroAppend } from "../../other/utils"; -export function OlLocation(props: { location: LatLng; className?: string; referenceSystem?: string; onClick?: () => void }) { +export function OlLocation(props: { location: LatLng; className?: string; referenceSystem?: string; onClick?: () => void; onRefSystemChange?: (refSystem: string) => any }) { const [referenceSystem, setReferenceSystem] = props.referenceSystem ? [props.referenceSystem, () => {}] : useState("LatLngDec"); const MGRS = latLngToMGRS(props.location.lat, props.location.lng, 6); if (referenceSystem === "MGRS") { @@ -17,6 +17,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere ? props.onClick : (ev) => { setReferenceSystem("LatLngDec"); + props.onRefSystemChange ? props.onRefSystemChange("LatLngDec") : null; ev.stopPropagation(); } } @@ -44,6 +45,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere ? props.onClick : (ev) => { setReferenceSystem("LatLngDMS"); + props.onRefSystemChange ? props.onRefSystemChange("LatLngDMS") : null; ev.stopPropagation(); } } @@ -83,6 +85,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere ? props.onClick : (ev) => { setReferenceSystem("LatLngDDM"); + props.onRefSystemChange ? props.onRefSystemChange("LatLngDDM") : null; ev.stopPropagation(); } } @@ -122,6 +125,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere ? props.onClick : (ev) => { setReferenceSystem("MGRS"); + props.onRefSystemChange ? props.onRefSystemChange("MGRS") : null; ev.stopPropagation(); } } diff --git a/frontend/react/src/ui/panels/coordinatespanel.tsx b/frontend/react/src/ui/panels/coordinatespanel.tsx index c1fe6dee..06489307 100644 --- a/frontend/react/src/ui/panels/coordinatespanel.tsx +++ b/frontend/react/src/ui/panels/coordinatespanel.tsx @@ -1,11 +1,12 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { OlLocation } from "../components/ollocation"; import { LatLng } from "leaflet"; -import { FaBullseye, FaChevronDown, FaChevronUp, FaJetFighter, FaMountain } from "react-icons/fa6"; -import { BullseyesDataChangedEvent, MouseMovedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events"; -import { computeBearingRangeString, mToFt } from "../../other/utils"; +import { FaBullseye, FaChevronDown, FaChevronUp, FaJetFighter, FaMountain, FaCopy, FaXmark } from "react-icons/fa6"; +import { BullseyesDataChangedEvent, CoordinatesFreezeEvent, MouseMovedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events"; +import { computeBearingRangeString, ConvertDDToDMS, DDToDDM, isTrustedEnvironment, latLngToMGRS, mToFt, zeroAppend } from "../../other/utils"; import { Bullseye } from "../../mission/bullseye"; import { Unit } from "../../unit/unit"; +import { getApp } from "../../olympusapp"; export function CoordinatesPanel(props: {}) { const [latlng, setLatlng] = useState(new LatLng(0, 0)); @@ -13,7 +14,10 @@ export function CoordinatesPanel(props: {}) { const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye }); const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); const [open, setOpen] = useState(true); - + const [copyCoordsOpen, setCopyCoordsOpen] = useState(false); + const [refSystem, setRefSystem] = useState("LatLngDec"); + const [copyableCoordinates, setCopyableCoordinates] = useState("To start, click any point on the map."); + useEffect(() => { MouseMovedEvent.on((latlng, elevation) => { setLatlng(latlng); @@ -23,18 +27,43 @@ export function CoordinatesPanel(props: {}) { BullseyesDataChangedEvent.on((bullseyes) => setBullseyes(bullseyes)); SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits(selectedUnits)); SelectionClearedEvent.on(() => setSelectedUnits([])); - }, []); + CoordinatesFreezeEvent.on( () => { + setCopyableCoordinates(getCopyableCoordinates()); + }); + }, [refSystem, latlng, elevation]); + + const getCopyableCoordinates = () => { + let returnString = ''; + + switch (refSystem) { + case "LatLngDec": + returnString = `${latlng.lat >= 0 ? "N" : "S"} ${zeroAppend(latlng.lat, 3, true, 6)}°, ${latlng.lng >= 0 ? "E" : "W"} ${zeroAppend(latlng.lng, 3, true, 6)}°,` + break; + case "LatLngDMS": + returnString = `${latlng.lat >= 0 ? "N" : "S"} ${ConvertDDToDMS(latlng.lat, false)}, ${latlng.lng >= 0 ? "E" : "W"} ${ConvertDDToDMS(latlng.lng, false)},` + break; + case "LatLngDDM": + returnString = `${latlng.lat >= 0 ? "N" : "S"} ${DDToDDM(latlng.lat)}, ${latlng.lng >= 0 ? "E" : "W"} ${DDToDDM(latlng.lng)}`; + break; + case "MGRS": + returnString = latLngToMGRS(latlng.lat, latlng.lng, 6)?.string || "Error"; + break; + } + + returnString += ` Elevation: ${Math.round(elevation)}`; + + return returnString; + }; return (
setOpen(!open)} > -
+
setOpen(!open)}> {open ? ( ) : ( @@ -44,11 +73,7 @@ export function CoordinatesPanel(props: {}) { )}
{open && bullseyes && ( -
+
- + setRefSystem(evt)} className={` + !min-w-64 !max-w-64 bg-transparent !p-0 + `} location={latlng} />
- {open && ( -
- +
+ + + +
{mToFt(elevation).toFixed()}ft
+
+ +
{ + evt.stopPropagation(); + setCopyCoordsOpen(true); + + if (isTrustedEnvironment()) { + try { + await navigator.clipboard.writeText(copyableCoordinates); + getApp().addInfoMessage(`Coordinates copied to clipboard: ${copyableCoordinates}`); + } catch (err) { + console.error('Failed to copy text: ', err); + } + } + }} > - - -
{mToFt(elevation).toFixed()}ft
-
- )} + + + +
Copy Coords
+
+
, + + open && copyCoordsOpen && ( +
evt.stopPropagation()} + > + + +
setCopyCoordsOpen(false)}> + +
+
+ ), + ]}
); } diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 0ee452ea..2dc6b4c1 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -417,9 +417,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "}
{entry[1][1] as string}
@@ -799,9 +797,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Hold fire: The unit will not shoot in any circumstance
@@ -809,9 +805,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Return fire: The unit will not fire unless fired upon
@@ -819,17 +813,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "}
{" "} Fire on target: The unit will not fire unless fired upon{" "}

or

{" "} @@ -840,9 +830,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Free: The unit will fire at any detected enemy in range
@@ -850,25 +838,19 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
Currently, DCS blue and red ground units do not respect{" "} {" "} and{" "} {" "} rules of engagement, so be careful, they may start shooting when you don't want them to. Use neutral units for finer control. @@ -929,9 +911,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} No reaction: The unit will not react in any circumstance
@@ -939,9 +919,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Passive: The unit will use counter-measures, but will not alter its course
@@ -949,9 +927,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Manouevre: The unit will try to evade the threat using manoeuvres, but no counter-measures @@ -959,9 +935,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Full evasion: the unit will try to evade the threat both manoeuvering and using counter-measures @@ -1016,9 +990,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Radio silence: No radar or ECM will be used @@ -1026,9 +998,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Defensive: The unit will turn radar and ECM on only when threatened @@ -1036,9 +1006,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Attack: The unit will use radar and ECM when engaging other units @@ -1046,9 +1014,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {" "} {" "} Free: the unit will use the radar and ECM all the time @@ -1269,9 +1235,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -1451,9 +1415,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { {/* ============== Operate as toggle START ============== */} {selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
void }) { >
Barrel height:{" "}
void }) { }} >
m
Muzzle velocity:{" "}
void }) { }} >
m/s
Aim time:{" "}
void }) { }} >
s
Shots to fire:{" "}
void }) {
Shots base interval:{" "}
void }) { }} >
s
Shots base scatter:{" "}
void }) { }} >
deg
Engagement range:{" "}
void }) { }} >
m
Targeting range:{" "}
void }) { }} >
m
Aim method range:{" "}
void }) { }} >
m
Acquisition range:{" "}
void }) { }} >
m
diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index eece5f48..1bb25f9d 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -60,6 +60,7 @@ import * as turf from "@turf/turf"; import { Carrier } from "../mission/carrier"; import { ContactsUpdatedEvent, + CoordinatesFreezeEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, UnitContextMenuRequestEvent, @@ -1611,6 +1612,7 @@ export abstract class Unit extends CustomMarker { } #onLeftShortClick(e: any) { + CoordinatesFreezeEvent.dispatch(); DomEvent.stop(e); DomEvent.preventDefault(e); e.originalEvent.stopImmediatePropagation();