From d35e6063e7e06eb70dd898180c86fe926237b8a4 Mon Sep 17 00:00:00 2001 From: MarcoJayUsai Date: Sat, 22 Mar 2025 10:21:08 +0100 Subject: [PATCH 1/8] feat(coordinates): added DDM format --- frontend/react/src/other/utils.ts | 7 +++ .../react/src/ui/components/ollocation.tsx | 43 ++++++++++++++++++- frontend/react/src/ui/modals/keybindmodal.tsx | 6 +-- frontend/react/src/ui/panels/jtacmenu.tsx | 3 ++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index df0b6577..d2b01d07 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -83,6 +83,13 @@ export function ConvertDDToDMS(D: number, lng: boolean) { else return zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + '"'; } +export function DDToDDM(decimalDegrees) { + const degrees = Math.trunc(decimalDegrees); + const minutes = Math.abs((decimalDegrees - degrees) * 60); + + return `${Math.abs(degrees)}° ${minutes.toFixed(4)}'`; +} + export function deg2rad(deg: number) { var pi = Math.PI; return deg * (pi / 180); diff --git a/frontend/react/src/ui/components/ollocation.tsx b/frontend/react/src/ui/components/ollocation.tsx index 34e142f5..f6a76ef2 100644 --- a/frontend/react/src/ui/components/ollocation.tsx +++ b/frontend/react/src/ui/components/ollocation.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { LatLng } from "leaflet"; -import { ConvertDDToDMS, latLngToMGRS, latLngToUTM, zeroAppend } from "../../other/utils"; +import { ConvertDDToDMS, DDToDDM, latLngToMGRS, latLngToUTM, zeroAppend } from "../../other/utils"; export function OlLocation(props: { location: LatLng; className?: string; referenceSystem?: string; onClick?: () => void }) { const [referenceSystem, setReferenceSystem] = props.referenceSystem ? [props.referenceSystem, () => {}] : useState("LatLngDec"); @@ -82,7 +82,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere props.onClick ? props.onClick : (ev) => { - setReferenceSystem("MGRS"); + setReferenceSystem("LatLngDDM"); ev.stopPropagation(); } } @@ -109,6 +109,45 @@ export function OlLocation(props: { location: LatLng; className?: string; refere ); + } else if (referenceSystem === "LatLngDDM") { + return ( +
{ + setReferenceSystem("MGRS"); + ev.stopPropagation(); + } + } + > +
+ + {props.location.lat >= 0 ? "N" : "S"} + + {DDToDDM(props.location.lat)} +
+
+ + {props.location.lng >= 0 ? "E" : "W"} + + {DDToDDM(props.location.lng)} +
+
+ ); } else { } } diff --git a/frontend/react/src/ui/modals/keybindmodal.tsx b/frontend/react/src/ui/modals/keybindmodal.tsx index fd98864d..bd4e8a65 100644 --- a/frontend/react/src/ui/modals/keybindmodal.tsx +++ b/frontend/react/src/ui/modals/keybindmodal.tsx @@ -62,7 +62,7 @@ export function KeybindModal(props: { open: boolean }) { return ( -
+
{inUseShortcuts.map((shortcut) => ( - {shortcut.getOptions().label} + {shortcut.getOptions().label} ))}
@@ -105,7 +105,7 @@ export function KeybindModal(props: { open: boolean }) {
)} -
+
{shortcut && (
@@ -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(); From 2fd875205003706d000b6c6b222c3539f2486db7 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Sat, 22 Mar 2025 19:03:02 +0100 Subject: [PATCH 3/8] feat: better handling of mods payloads --- manager/javascripts/filesystem.js | 6 ++++++ scripts/lua/backend/OlympusCommand.lua | 4 +++- scripts/lua/backend/mods.lua | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/manager/javascripts/filesystem.js b/manager/javascripts/filesystem.js index 9b6b0634..12bd1117 100644 --- a/manager/javascripts/filesystem.js +++ b/manager/javascripts/filesystem.js @@ -60,6 +60,7 @@ async function installHooks(folder) { */ async function installMod(folder, name) { + /* Timestamp string */ logger.log(`Installing mod in ${folder}`) await fsp.cp(path.join("..", "mod"), path.join(folder, "Mods", "Services", "Olympus"), { recursive: true }); @@ -242,6 +243,11 @@ async function deleteMod(folder, name) { else logger.warn(`No mods.lua found in ${folder}, skipping backup...`) + if (await exists(path.join(folder, "Mods", "Services", "Olympus", "scripts", "unitPayloads.lua"))) + await fsp.cp(path.join(folder, "Mods", "Services", "Olympus", "scripts", "unitPayloads.lua"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "unitPayloads.lua")); + else + logger.warn(`No unitPayloads.lua found in ${folder}, skipping backup...`) + /* Remove the mod folder */ await fsp.rmdir(path.join(folder, "Mods", "Services", "Olympus"), { recursive: true, force: true }) logger.log(`Mod succesfully removed from ${folder}`) diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index 44f27268..96bb07d5 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -659,7 +659,7 @@ end -- lat: (number) -- lng: (number) -- alt: (number, optional) only for air units - -- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua + -- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua or mods.lua -- payload: (table, optional) overrides loadout, specifies directly the loadout of the unit -- liveryID: (string, optional) function Olympus.spawnUnits(spawnTable) @@ -731,6 +731,8 @@ function Olympus.generateAirUnitsTable(units) if payload == nil then if loadout ~= nil and loadout ~= "" and Olympus.unitPayloads[unit.unitType] and Olympus.unitPayloads[unit.unitType][loadout] then payload = { ["pylons"] = Olympus.unitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } + if loadout ~= nil and loadout ~= "" and Olympus.modsUnitPayloads ~= nil and Olympus.modsUnitPayloads[unit.unitType] and Olympus.modsUnitPayloads[unit.unitType][loadout] then + payload = { ["pylons"] = Olympus.modsUnitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } else payload = { ["pylons"] = {}, ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } end diff --git a/scripts/lua/backend/mods.lua b/scripts/lua/backend/mods.lua index 4107733b..82b093d6 100644 --- a/scripts/lua/backend/mods.lua +++ b/scripts/lua/backend/mods.lua @@ -9,3 +9,19 @@ Olympus.modsList = { ["A-4E-C"] = "Aircraft", ["Bronco-OV-10A"] = "Aircraft" } + +-- Enter here any unitPayloads you want to use for your mods. Remember to add the payload to the database in mods.json! +-- DO NOT ADD PAYLOADS TO "ORIGINAL" DCS UNITS HERE! To add payloads to original DCS units, use the "unitPayload.lua" table instead and add them under the correct unit section. +-- Provided example is for the A-4E-C mod, with a payload of 76 FFAR Mk1 HE rockets and a 300 gallon fuel tank. + +Olympus.modsUnitPayloads = { + ["A-4E-C"] = { + ["FFAR Mk1 HE *76, Fuel 300G"] = { + [1] = {["CLSID"] = "{LAU3_FFAR_MK1HE}"}, + [2] = {["CLSID"] = "{LAU3_FFAR_MK1HE}"}, + [3] = {["CLSID"] = "{LAU3_FFAR_MK1HE}"}, + [4] = {["CLSID"] = "{LAU3_FFAR_MK1HE}"}, + [5] = {["CLSID"] = "{DFT-300gal}"} + } + } +} From c33afa5215330f62c71929d3541f5ff1c8713229 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Sat, 22 Mar 2025 19:27:41 +0100 Subject: [PATCH 4/8] fix: Humans can be exploded --- frontend/react/src/ui/ui.tsx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index ff53ff02..394a5f1a 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -103,15 +103,13 @@ export function UI() { getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} + onClose={() => {}} /> {/*} getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} />{*/} - - @@ -131,15 +129,15 @@ export function UI() { backdrop-blur-sm `} > -
+
- Olympus Logo + Olympus Logo
{!connectedOnce &&
Establishing connection
} {connectedOnce &&
Connection lost
} From 8eaebb5d2235512d6018473d939c8894d2cc4363 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Sun, 23 Mar 2025 15:40:48 +0100 Subject: [PATCH 5/8] chore: updated setup-node action --- .github/workflows/build_package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_package.yml b/.github/workflows/build_package.yml index 53b9ee02..9bc1932a 100644 --- a/.github/workflows/build_package.yml +++ b/.github/workflows/build_package.yml @@ -23,7 +23,7 @@ jobs: vcpkg integrate install - name: Setup Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 - name: Build working-directory: . From c66086d64f9deec8b4f8d854de8f80e0a529a7f0 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Sun, 23 Mar 2025 16:06:38 +0100 Subject: [PATCH 6/8] fix: missing else in mod units code --- scripts/lua/backend/OlympusCommand.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index 96bb07d5..18cc95d6 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -731,7 +731,7 @@ function Olympus.generateAirUnitsTable(units) if payload == nil then if loadout ~= nil and loadout ~= "" and Olympus.unitPayloads[unit.unitType] and Olympus.unitPayloads[unit.unitType][loadout] then payload = { ["pylons"] = Olympus.unitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } - if loadout ~= nil and loadout ~= "" and Olympus.modsUnitPayloads ~= nil and Olympus.modsUnitPayloads[unit.unitType] and Olympus.modsUnitPayloads[unit.unitType][loadout] then + elseif loadout ~= nil and loadout ~= "" and Olympus.modsUnitPayloads ~= nil and Olympus.modsUnitPayloads[unit.unitType] and Olympus.modsUnitPayloads[unit.unitType][loadout] then payload = { ["pylons"] = Olympus.modsUnitPayloads[unit.unitType][loadout], ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } else payload = { ["pylons"] = {}, ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100 } From 0b8fb969b288726675cf0592430ac583ad4ff09d Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Sun, 23 Mar 2025 16:50:34 +0100 Subject: [PATCH 7/8] fix: Added option to upload hidden files --- .github/workflows/build_package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_package.yml b/.github/workflows/build_package.yml index 9bc1932a..b6c24278 100644 --- a/.github/workflows/build_package.yml +++ b/.github/workflows/build_package.yml @@ -41,5 +41,6 @@ jobs: with: name: zip_only_package path: ./zip + include-hidden-files: true From eff96ce0c2f649f8ada4d965370eea6929c2c38a Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Mon, 24 Mar 2025 16:55:40 +0100 Subject: [PATCH 8/8] feat: Control scheme improvement --- frontend/react/src/map/boxselect.ts | 78 ++- frontend/react/src/map/map.ts | 477 +++++++++--------- frontend/react/src/map/mapMouseHandler.ts | 198 ++++++++ frontend/react/src/other/utils.ts | 4 + frontend/react/src/server/servermanager.ts | 3 +- .../src/ui/contextmenus/mapcontextmenu.tsx | 6 +- .../react/src/ui/panels/controlspanel.tsx | 16 +- frontend/react/src/ui/panels/infobar.tsx | 3 +- frontend/react/src/ui/panels/maptoolbar.tsx | 8 +- frontend/react/src/unit/unit.ts | 37 +- frontend/react/src/unit/unitsmanager.ts | 10 +- 11 files changed, 554 insertions(+), 286 deletions(-) create mode 100644 frontend/react/src/map/mapMouseHandler.ts diff --git a/frontend/react/src/map/boxselect.ts b/frontend/react/src/map/boxselect.ts index f4829029..55a9d8f1 100644 --- a/frontend/react/src/map/boxselect.ts +++ b/frontend/react/src/map/boxselect.ts @@ -16,13 +16,13 @@ export var BoxSelect = Handler.extend({ }, addHooks: function () { - DomEvent.on(this._container, "mousedown", this._onMouseDown, this); - DomEvent.on(this._container, "touchstart", this._onMouseDown, this); + if ("ontouchstart" in window) DomEvent.on(this._container, "touchend", this._onMouseDown, this); + else DomEvent.on(this._container, "mousedown", this._onMouseDown, this); }, removeHooks: function () { - DomEvent.off(this._container, "mousedown", this._onMouseDown, this); - DomEvent.off(this._container, "touchend", this._onMouseDown, this); + if ("ontouchstart" in window) DomEvent.off(this._container, "touchstart", this._onMouseDown, this); + else DomEvent.off(this._container, "mousedown", this._onMouseDown, this); }, moved: function () { @@ -48,18 +48,29 @@ export var BoxSelect = Handler.extend({ if (e.type === "touchstart") this._startPoint = this._map.mouseEventToContainerPoint(e.touches[0]); else this._startPoint = this._map.mouseEventToContainerPoint(e); - DomEvent.on( - //@ts-ignore - document, - { - contextmenu: DomEvent.stop, - touchmove: this._onMouseMove, - touchend: this._onMouseUp, - mousemove: this._onMouseMove, - mouseup: this._onMouseUp, - }, - this - ); + if ("ontouchstart" in window) { + DomEvent.on( + //@ts-ignore + document, + { + contextmenu: DomEvent.stop, + touchmove: this._onMouseMove, + touchend: this._onMouseUp, + }, + this + ); + } else { + DomEvent.on( + //@ts-ignore + document, + { + contextmenu: DomEvent.stop, + mousemove: this._onMouseMove, + mouseup: this._onMouseUp, + }, + this + ); + } } else { return false; } @@ -109,18 +120,29 @@ export var BoxSelect = Handler.extend({ DomUtil.enableImageDrag(); this._map.dragging.enable(); - DomEvent.off( - //@ts-ignore - document, - { - contextmenu: DomEvent.stop, - touchmove: this._onMouseMove, - touchend: this._onMouseUp, - mousemove: this._onMouseMove, - mouseup: this._onMouseUp, - }, - this - ); + if ("ontouchstart" in window) { + DomEvent.off( + //@ts-ignore + document, + { + contextmenu: DomEvent.stop, + touchmove: this._onMouseMove, + touchend: this._onMouseUp, + }, + this + ); + } else { + DomEvent.off( + //@ts-ignore + document, + { + contextmenu: DomEvent.stop, + mousemove: this._onMouseMove, + mouseup: this._onMouseUp, + }, + this + ); + } this._resetState(); }, diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index d146c1c9..a5b89d3f 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -3,7 +3,7 @@ import { getApp } from "../olympusapp"; import { BoxSelect } from "./boxselect"; import { Airbase } from "../mission/airbase"; import { Unit } from "../unit/unit"; -import { areaContains, deepCopyTable, deg2rad, getGroundElevation } from "../other/utils"; +import { areaContains, deepCopyTable, deg2rad, getGroundElevation, getMagvar, rad2deg } from "../other/utils"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { @@ -70,6 +70,7 @@ import { ContextActionSet } from "../unit/contextactionset"; import { SmokeMarker } from "./markers/smokemarker"; import { Measure } from "./measure"; import { FlakMarker } from "./markers/flakmarker"; +import { MapMouseHandler } from "./mapMouseHandler"; /* Register the handler for the box selection */ L.Map.addInitHook("addHandler", "boxSelect", BoxSelect); @@ -110,14 +111,8 @@ export class Map extends L.Map { #isDragging: boolean = false; #isSelecting: boolean = false; - #originalMouseClickLatLng: L.LatLng | null = null; - #debounceTimeout: number | null = null; - #isLeftMouseDown: boolean = false; - #isRightMouseDown: boolean = false; - #leftMouseDownEpoch: number = 0; - #rightMouseDownEpoch: number = 0; - #leftMouseDownTimeout: number = 0; - #rightMouseDownTimeout: number = 0; + #mouseHandler: MapMouseHandler = new MapMouseHandler(this); + #lastMousePosition: L.Point = new L.Point(0, 0); #lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0); #previousZoom: number = 0; @@ -139,7 +134,7 @@ export class Map extends L.Map { #destinationPreviewMarkers: { [key: number]: TemporaryUnitMarker | TargetMarker } = {}; #destinationRotation: number = 0; #isRotatingDestination: boolean = false; - #destionationWasRotated: boolean = false; + #destinationRotationCenter: L.LatLng = new L.LatLng(0, 0); /* Unit context actions */ #contextActionSet: null | ContextActionSet = null; @@ -209,22 +204,20 @@ export class Map extends L.Map { this.on("selectionstart", (e: any) => this.#onSelectionStart(e)); this.on("selectionend", (e: any) => this.#onSelectionEnd(e)); - this.on("mouseup", (e: any) => this.#onMouseUp(e)); - this.on("touchend", (e: any) => this.#onMouseUp(e)); - this.on("mousedown", (e: any) => this.#onMouseDown(e)); - this.on("touchstart", (e: any) => this.#onMouseDown(e)); - this.on("dblclick", (e: any) => this.#onDoubleClick(e)); - this.on("click", (e: any) => e.originalEvent.preventDefault()); - this.on("contextmenu", (e: any) => e.originalEvent.preventDefault()); - - this.on("mousemove", (e: any) => this.#onMouseMove(e)); - this.on("move", (e: any) => this.#onMapMove(e)); - /* Custom touch events for touchscreen support */ - L.DomEvent.on(this.getContainer(), "touchstart", this.#onMouseDown, this); - L.DomEvent.on(this.getContainer(), "touchend", this.#onMouseUp, this); - L.DomEvent.on(this.getContainer(), "wheel", this.#onMouseWheel, this); + this.#mouseHandler.leftMousePressed = (e: L.LeafletMouseEvent) => this.#onLeftMousePressed(e); + this.#mouseHandler.leftMouseReleased = (e: L.LeafletMouseEvent) => this.#onLeftMouseReleased(e); + this.#mouseHandler.rightMousePressed = (e: L.LeafletMouseEvent) => this.#onRightMousePressed(e); + this.#mouseHandler.rightMouseReleased = (e: L.LeafletMouseEvent) => this.#onRightMouseReleased(e); + this.#mouseHandler.mouseWheelPressed = (e: L.LeafletMouseEvent) => this.#onMouseWheelPressed(e); + this.#mouseHandler.mouseWheelReleased = (e: L.LeafletMouseEvent) => this.#onMouseWheelReleased(e); + this.#mouseHandler.leftMouseShortClick = (e: L.LeafletMouseEvent) => this.#onLeftMouseShortClick(e); + this.#mouseHandler.rightMouseShortClick = (e: L.LeafletMouseEvent) => this.#onRightMouseShortClick(e); + this.#mouseHandler.leftMouseLongClick = (e: L.LeafletMouseEvent) => this.#onLeftMouseLongClick(e); + this.#mouseHandler.rightMouseLongClick = (e: L.LeafletMouseEvent) => this.#onRightMouseLongClick(e); + this.#mouseHandler.leftMouseDoubleClick = (e: L.LeafletMouseEvent) => this.#onLeftMouseDoubleClick(e); + this.#mouseHandler.mouseMove = (e: L.LeafletMouseEvent) => this.#onMouseMove(e); /* Event listeners */ AppStateChangedEvent.on((state, subState) => this.#onStateChanged(state, subState)); @@ -405,14 +398,14 @@ export class Map extends L.Map { altKey: false, ctrlKey: false, }) - .addShortcut("toggleRelativePositions", { - label: "Toggle group movement mode", - keyUpCallback: () => this.setKeepRelativePositions(false), - keyDownCallback: () => this.setKeepRelativePositions(true), - code: "AltLeft", - shiftKey: false, - ctrlKey: false, - }) + //.addShortcut("toggleRelativePositions", { + // label: "Toggle group movement mode", + // keyUpCallback: () => this.setKeepRelativePositions(false), + // keyDownCallback: () => this.setKeepRelativePositions(true), + // code: "AltLeft", + // shiftKey: false, + // ctrlKey: false, + //}) .addShortcut("toggleSelectionEnabled", { label: "Toggle box selection", keyUpCallback: () => this.setSelectionEnabled(false), @@ -510,7 +503,7 @@ export class Map extends L.Map { code: "ShiftLeft", altKey: false, ctrlKey: false, - }) + }); } setLayerName(layerName: string) { @@ -803,8 +796,6 @@ export class Map extends L.Map { setKeepRelativePositions(keepRelativePositions: boolean) { this.#keepRelativePositions = keepRelativePositions; this.#updateDestinationPreviewMarkers(); - if (keepRelativePositions) this.scrollWheelZoom.disable(); - else this.scrollWheelZoom.enable(); } getKeepRelativePositions() { @@ -960,7 +951,10 @@ export class Map extends L.Map { } #onDragEnd(e: any) { - this.#isDragging = false; + /* Delay the drag end event so that any other event in the queue still sees the map in dragging mode */ + window.setTimeout(() => { + this.#isDragging = false; + }, SHORT_PRESS_MILLISECONDS + 100); } #onSelectionStart(e: any) { @@ -976,201 +970,219 @@ export class Map extends L.Map { /* Delay the event so that any other event in the queue still sees the map in selection mode */ window.setTimeout(() => { this.#isSelecting = false; - }, 300); + }, SHORT_PRESS_MILLISECONDS + 100); } - #onMouseUp(e: any) { + #onLeftMouseReleased(e: any) { this.dragging.enable(); - if (e.originalEvent?.button === 0) { - if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onLeftShortClick(e); - this.#isLeftMouseDown = false; - } else if (e.originalEvent?.button === 2) { - if (Date.now() - this.#rightMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onRightShortClick(e); - this.#isRightMouseDown = false; - } else if (e.originalEvent?.button === 1) { - getApp().setState(getApp().getState() === OlympusState.MEASURE ? OlympusState.IDLE : OlympusState.MEASURE); - if (getApp().getState() === OlympusState.MEASURE) { - const newMeasure = new Measure(this); - const previousMeasure = this.#measures[this.#measures.length - 1]; - this.#measures.push(newMeasure); - newMeasure.onClick(e.latlng); - if (previousMeasure && previousMeasure.isActive()) { - previousMeasure.finish(); - previousMeasure.hideEndMarker(); - newMeasure.onMarkerMoved = (startLatLng, endLatLng) => { - previousMeasure.moveMarkers(null, startLatLng); - }; - } + if (this.#isRotatingDestination && getApp().getState() === OlympusState.UNIT_CONTROL && this.getContextAction() !== null) { + this.executeContextAction(null, this.#destinationRotationCenter, e.originalEvent); + } + this.#isRotatingDestination = false; + this.setKeepRelativePositions(false); + + /* Delay the event so that any other event in the queue still sees the map in selection mode */ + window.setTimeout(() => { + this.setSelectionEnabled(false); + this.#isSelecting = false; + }, SHORT_PRESS_MILLISECONDS + 100); + } + + #onMouseWheelReleased(e: any) { + this.dragging.enable(); + + getApp().setState(getApp().getState() === OlympusState.MEASURE ? OlympusState.IDLE : OlympusState.MEASURE); + if (getApp().getState() === OlympusState.MEASURE) { + const newMeasure = new Measure(this); + const previousMeasure = this.#measures[this.#measures.length - 1]; + this.#measures.push(newMeasure); + newMeasure.onClick(e.latlng); + if (previousMeasure && previousMeasure.isActive()) { + previousMeasure.finish(); + previousMeasure.hideEndMarker(); + newMeasure.onMarkerMoved = (startLatLng, endLatLng) => { + previousMeasure.moveMarkers(null, startLatLng); + }; } } } - #onMouseDown(e: any) { - if (e.originalEvent?.button === 1) { - this.dragging.disable(); - } // Disable dragging when right clicking + #onRightMouseReleased(e: any) { + this.dragging.enable(); - this.#originalMouseClickLatLng = e.latlng; - if (e.originalEvent?.button === 0) { - this.#isLeftMouseDown = true; - this.#leftMouseDownEpoch = Date.now(); - } else if (e.originalEvent?.button === 2) { - this.#isRightMouseDown = true; - this.#rightMouseDownEpoch = Date.now(); - this.#rightMouseDownTimeout = window.setTimeout(() => { - this.#onRightLongClick(e); - }, SHORT_PRESS_MILLISECONDS); + if (this.#isRotatingDestination && getApp().getState() === OlympusState.UNIT_CONTROL) { + this.executeDefaultContextAction(null, this.#destinationRotationCenter, e.originalEvent); + } + this.#isRotatingDestination = false; + this.setKeepRelativePositions(false); + } + + #onLeftMousePressed(e: any) { + if (getApp().getState() === OlympusState.UNIT_CONTROL && getApp().getSubState() === UnitControlSubState.MAP_CONTEXT_MENU) { + getApp().setState(OlympusState.UNIT_CONTROL); } } - #onMouseWheel(e: any) { - if (this.#keepRelativePositions) { - this.#destinationRotation += e.deltaY / 20; - this.#moveDestinationPreviewMarkers(); - } + #onMouseWheelPressed(e: any) {} + + #onRightMousePressed(e: any) { + this.dragging.disable(); } - #onLeftShortClick(e: L.LeafletMouseEvent) { + #onLeftMouseShortClick(e: L.LeafletMouseEvent) { CoordinatesFreezeEvent.dispatch(); - if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) { - this.#debounceTimeout = window.setTimeout(() => { - if (!this.#isSelecting) { - console.log(`Left short click at ${e.latlng}`); - if (this.#pasteEnabled) { - getApp().getUnitsManager().paste(e.latlng); - } + if (this.#isDragging || this.#isSelecting) return; + console.log(`Left short click at ${e.latlng}`); - /* Execute the short click action */ - if (getApp().getState() === OlympusState.IDLE) { - /* Do nothing */ - } else if (getApp().getState() === OlympusState.SPAWN) { - if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) { - if (this.#spawnRequestTable !== null) { - this.#spawnRequestTable.unit.location = e.latlng; - this.#spawnRequestTable.unit.heading = deg2rad(this.#spawnHeading); - getApp() - .getUnitsManager() - .spawnUnits( - this.#spawnRequestTable.category, - Array(this.#spawnRequestTable.amount).fill(this.#spawnRequestTable.unit), - this.#spawnRequestTable.coalition, - false, - undefined, - undefined, - (hash) => { - this.addTemporaryMarker( - e.latlng, - this.#spawnRequestTable?.unit.unitType ?? "unknown", - this.#spawnRequestTable?.coalition ?? "blue", - false, - hash - ); - } - ); + if (this.#pasteEnabled) { + getApp().getUnitsManager().paste(e.latlng); + } + + /* Execute the short click action */ + if (getApp().getState() === OlympusState.IDLE) { + /* Do nothing */ + } else if (getApp().getState() === OlympusState.SPAWN) { + if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) { + if (this.#spawnRequestTable !== null) { + this.#spawnRequestTable.unit.location = e.latlng; + this.#spawnRequestTable.unit.heading = deg2rad(this.#spawnHeading); + getApp() + .getUnitsManager() + .spawnUnits( + this.#spawnRequestTable.category, + Array(this.#spawnRequestTable.amount).fill(this.#spawnRequestTable.unit), + this.#spawnRequestTable.coalition, + false, + undefined, + undefined, + (hash) => { + this.addTemporaryMarker( + e.latlng, + this.#spawnRequestTable?.unit.unitType ?? "unknown", + this.#spawnRequestTable?.coalition ?? "blue", + false, + hash + ); } - } else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) { - if (this.#effectRequestTable !== null) { - if (this.#effectRequestTable.type === "explosion") { - if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", e.latlng); - else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", e.latlng); - else if (this.#effectRequestTable.explosionType === "White phosphorous") - getApp().getServerManager().spawnExplosion(50, "phosphorous", e.latlng); + ); + } + } else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) { + if (this.#effectRequestTable !== null) { + if (this.#effectRequestTable.type === "explosion") { + if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", e.latlng); + else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", e.latlng); + else if (this.#effectRequestTable.explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", e.latlng); - this.addExplosionMarker(e.latlng); - } else if (this.#effectRequestTable.type === "smoke") { - getApp() - .getServerManager() - .spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", e.latlng); - this.addSmokeMarker(e.latlng, this.#effectRequestTable.smokeColor ?? "white"); - } - } - } - } else if (getApp().getState() === OlympusState.DRAW) { - getApp().getCoalitionAreasManager().onLeftShortClick(e); - } else if (getApp().getState() === OlympusState.JTAC) { - // TODO less redundant way to do this - if (getApp().getSubState() === JTACSubState.SELECT_TARGET) { - if (!this.#targetPoint) { - this.#targetPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); - this.#targetPoint.addTo(this); - this.#targetPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#targetPoint.on("dragend", (event) => { - getApp().setState(OlympusState.JTAC); - event.target.options["freeze"] = false; - }); - this.#targetPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC); - }); - } else this.#targetPoint.setLatLng(e.latlng); - } else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) { - if (!this.#ECHOPoint) { - this.#ECHOPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); - this.#ECHOPoint.addTo(this); - this.#ECHOPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#ECHOPoint.on("dragend", (event) => { - getApp().setState(OlympusState.JTAC); - event.target.options["freeze"] = false; - }); - this.#ECHOPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC); - }); - } else this.#ECHOPoint.setLatLng(e.latlng); - } else if (getApp().getSubState() === JTACSubState.SELECT_IP) { - if (!this.#IPPoint) { - this.#IPPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); - this.#IPPoint.addTo(this); - this.#IPPoint.on("dragstart", (event) => { - event.target.options["freeze"] = true; - }); - this.#IPPoint.on("dragend", (event) => { - getApp().setState(OlympusState.JTAC); - event.target.options["freeze"] = false; - }); - this.#IPPoint.on("click", (event) => { - getApp().setState(OlympusState.JTAC); - }); - } else this.#IPPoint.setLatLng(e.latlng); - } - getApp().setState(OlympusState.JTAC); - this.#drawIPToTargetLine(); - } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent); - else if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE); - else getApp().setState(OlympusState.UNIT_CONTROL); - } else if (getApp().getState() === OlympusState.MEASURE) { - const newMeasure = new Measure(this); - const previousMeasure = this.#measures[this.#measures.length - 1]; - this.#measures.push(newMeasure); - newMeasure.onClick(e.latlng); - if (previousMeasure && previousMeasure.isActive()) { - previousMeasure.finish(); - previousMeasure.hideEndMarker(); - newMeasure.onMarkerMoved = (startLatLng, endLatLng) => { - previousMeasure.moveMarkers(null, startLatLng); - }; - } - } else { - if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE); - else getApp().setState(OlympusState.UNIT_CONTROL); + this.addExplosionMarker(e.latlng); + } else if (this.#effectRequestTable.type === "smoke") { + getApp() + .getServerManager() + .spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", e.latlng); + this.addSmokeMarker(e.latlng, this.#effectRequestTable.smokeColor ?? "white"); } } - - if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout); - this.#debounceTimeout = null; - }, DEBOUNCE_MILLISECONDS); + } + } else if (getApp().getState() === OlympusState.DRAW) { + getApp().getCoalitionAreasManager().onLeftShortClick(e); + } else if (getApp().getState() === OlympusState.JTAC) { + // TODO less redundant way to do this + if (getApp().getSubState() === JTACSubState.SELECT_TARGET) { + if (!this.#targetPoint) { + this.#targetPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#targetPoint.addTo(this); + this.#targetPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#targetPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#targetPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#targetPoint.setLatLng(e.latlng); + } else if (getApp().getSubState() === JTACSubState.SELECT_ECHO_POINT) { + if (!this.#ECHOPoint) { + this.#ECHOPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#ECHOPoint.addTo(this); + this.#ECHOPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#ECHOPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#ECHOPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#ECHOPoint.setLatLng(e.latlng); + } else if (getApp().getSubState() === JTACSubState.SELECT_IP) { + if (!this.#IPPoint) { + this.#IPPoint = new TextMarker(e.latlng, "BP", "rgb(37 99 235)", { interactive: true, draggable: true }); + this.#IPPoint.addTo(this); + this.#IPPoint.on("dragstart", (event) => { + event.target.options["freeze"] = true; + }); + this.#IPPoint.on("dragend", (event) => { + getApp().setState(OlympusState.JTAC); + event.target.options["freeze"] = false; + }); + this.#IPPoint.on("click", (event) => { + getApp().setState(OlympusState.JTAC); + }); + } else this.#IPPoint.setLatLng(e.latlng); + } + getApp().setState(OlympusState.JTAC); + this.#drawIPToTargetLine(); + } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { + if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent); + else if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE); + else getApp().setState(OlympusState.UNIT_CONTROL); + } else if (getApp().getState() === OlympusState.MEASURE) { + const newMeasure = new Measure(this); + const previousMeasure = this.#measures[this.#measures.length - 1]; + this.#measures.push(newMeasure); + newMeasure.onClick(e.latlng); + if (previousMeasure && previousMeasure.isActive()) { + previousMeasure.finish(); + previousMeasure.hideEndMarker(); + newMeasure.onMarkerMoved = (startLatLng, endLatLng) => { + previousMeasure.moveMarkers(null, startLatLng); + }; + } + } else { + if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE); + else getApp().setState(OlympusState.UNIT_CONTROL); } } - #onRightShortClick(e: L.LeafletMouseEvent) { - console.log(`Right short click at ${e.latlng}`); + #onLeftMouseLongClick(e: any) { + if (this.#isDragging || this.#isSelecting) return; + console.log(`Left long click at ${e.latlng}`); - window.clearTimeout(this.#rightMouseDownTimeout); + if (getApp().getState() === OlympusState.UNIT_CONTROL) { + if (!this.getContextAction()) { + getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); + MapContextMenuRequestEvent.dispatch(e.latlng); + } else { + this.#destinationRotationCenter = e.latlng; + this.#isRotatingDestination = true; + this.setKeepRelativePositions(true); + this.dragging.disable(); + } + } else { + getApp().setState(OlympusState.IDLE); + this.setSelectionEnabled(true); + + //@ts-ignore We force the boxselect to enter in selection mode + this.boxSelect._onMouseDown(e.originalEvent); + } + } + + #onRightMouseShortClick(e: L.LeafletMouseEvent) { + console.log(`Right short click at ${e.latlng}`); if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.SPAWN_CONTEXT) { SpawnContextMenuRequestEvent.dispatch(e.latlng); @@ -1180,22 +1192,17 @@ export class Map extends L.Map { } } - #onRightLongClick(e: L.LeafletMouseEvent) { + #onRightMouseLongClick(e: L.LeafletMouseEvent) { console.log(`Right long click at ${e.latlng}`); - if (getApp().getState() === OlympusState.UNIT_CONTROL) { - if (!this.getContextAction()) { - getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); - MapContextMenuRequestEvent.dispatch(e.latlng); - } - } + this.#destinationRotationCenter = e.latlng; + this.#isRotatingDestination = true; + this.setKeepRelativePositions(true); } - #onDoubleClick(e: L.LeafletMouseEvent) { + #onLeftMouseDoubleClick(e: L.LeafletMouseEvent) { console.log(`Double click at ${e.latlng}`); - if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout); - this.setPasteEnabled(false); if (getApp().getState() === OlympusState.DRAW) { @@ -1211,7 +1218,6 @@ export class Map extends L.Map { #onMouseMove(e: any) { if (!this.#isRotatingDestination) { - this.#destionationWasRotated = false; this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; this.#lastMouseCoordinates = e.latlng; @@ -1225,18 +1231,33 @@ export class Map extends L.Map { if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng); if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng); } else if (getApp().getState() === OlympusState.MEASURE) { - if (this.#debounceTimeout === null) { - this.#measures[this.#measures.length - 1]?.onMouseMove(e.latlng); - let totalLength = 0; - this.#measures.forEach((measure) => { - measure.setTotalDistance(totalLength); - totalLength += measure.getDistance(); - }); - } + //if (this.#debounceTimeout === null) { + this.#measures[this.#measures.length - 1]?.onMouseMove(e.latlng); + let totalLength = 0; + this.#measures.forEach((measure) => { + measure.setTotalDistance(totalLength); + totalLength += measure.getDistance(); + }); + //} } } else { - this.#destionationWasRotated = true; - this.#destinationRotation -= e.originalEvent.movementX; + if (this.#destinationRotationCenter) { + /* Compute the average heading of the units */ + const selectedUnits = getApp() + .getUnitsManager() + .getSelectedUnits() + .filter((unit) => !unit.getHuman()); + + let averageHeading = 0; + selectedUnits.forEach((unit) => { + averageHeading += unit.getHeading(); + }); + averageHeading /= selectedUnits.length; + + /* Compute the rotation of the destination */ + let angle = Math.atan2(e.latlng.lng - this.#destinationRotationCenter.lng, e.latlng.lat - this.#destinationRotationCenter.lat); + this.#destinationRotation = -(rad2deg(angle) - getMagvar(e.latlng.lat, e.latlng.lng) - rad2deg(averageHeading)); + } } if (getApp().getState() === OlympusState.DRAW && (getApp().getSubState() === DrawSubState.NO_SUBSTATE || getApp().getSubState() === DrawSubState.EDIT)) { @@ -1345,7 +1366,7 @@ export class Map extends L.Map { #moveDestinationPreviewMarkers() { if (this.#keepRelativePositions) { - Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#lastMouseCoordinates, this.#destinationRotation)).forEach(([ID, latlng]) => { + Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#destinationRotationCenter, this.#destinationRotation)).forEach(([ID, latlng]) => { this.#destinationPreviewMarkers[ID]?.setLatLng(latlng); }); } else { diff --git a/frontend/react/src/map/mapMouseHandler.ts b/frontend/react/src/map/mapMouseHandler.ts new file mode 100644 index 00000000..d99193ab --- /dev/null +++ b/frontend/react/src/map/mapMouseHandler.ts @@ -0,0 +1,198 @@ +import { DomEvent, LeafletMouseEvent, Point } from "leaflet"; +import { Map } from "./map"; + +enum MapMouseHandlerState { + IDLE = "IDLE", + LEFT_MOUSE_DOWN = "Left mouse down", + MOUSE_WHEEL_DOWN = "Mouse wheel down", + RIGHT_MOUSE_DOWN = "Right mouse down", + DEBOUNCING = "Debouncing", +} + +export class MapMouseHandler { + #map: Map; + #state: string = MapMouseHandlerState.IDLE; + #leftMouseDownEpoch: number = 0; + #rightMouseDownEpoch: number = 0; + #mouseWheelDownEpoch: number = 0; + #leftMouseDownTimeout: number | null = null; + #rightMouseDownTimeout: number | null = null; + #mouseWheelDownTimeout: number | null = null; + + #debounceTimeout: number | null = null; + + leftMousePressed: (event: LeafletMouseEvent) => void = () => {}; + leftMouseReleased: (event: LeafletMouseEvent) => void = () => {}; + rightMousePressed: (event: LeafletMouseEvent) => void = () => {}; + rightMouseReleased: (event: LeafletMouseEvent) => void = () => {}; + mouseWheelPressed: (event: LeafletMouseEvent) => void = () => {}; + mouseWheelReleased: (event: LeafletMouseEvent) => void = () => {}; + + leftMouseDoubleClick: (event: LeafletMouseEvent) => void = () => {}; + + leftMouseShortClick: (event: LeafletMouseEvent) => void = () => {}; + rightMouseShortClick: (event: LeafletMouseEvent) => void = () => {}; + mouseWheelShortClick: (event: LeafletMouseEvent) => void = () => {}; + leftMouseLongClick: (event: LeafletMouseEvent) => void = () => {}; + rightMouseLongClick: (event: LeafletMouseEvent) => void = () => {}; + mouseWheelLongClick: (event: LeafletMouseEvent) => void = () => {}; + + mouseMove: (event: LeafletMouseEvent) => void = () => {}; + + mouseWheel: (event: LeafletMouseEvent) => void = () => {}; + + constructor(map) { + this.#map = map; + + /* Events for touchscreen and mouse */ + if ("ontouchstart" in window) { + DomEvent.on(this.#map.getContainer(), "touchstart", (e: any) => this.#onTouchDown(e), this); + DomEvent.on(this.#map.getContainer(), "touchend", (e: any) => this.#onTouchUp(e), this); + DomEvent.on(this.#map.getContainer(), "touchmove", (e: any) => this.#onTouchMove(e), this); + } else { + this.#map.on("mouseup", (e: any) => this.#onMouseUp(e)); + this.#map.on("mousedown", (e: any) => this.#onMouseDown(e)); + this.#map.on("mousemove", (e: any) => this.#onMouseMove(e)); + } + this.#map.on("dblclick", (e: any) => this.#onDoubleClick(e)); + + /* Disable unwanted events */ + this.#map.on("click", (e: any) => e.originalEvent.preventDefault()); + this.#map.on("contextmenu", (e: any) => e.originalEvent.preventDefault()); + + /* Mouse wheel event */ + DomEvent.on(this.#map.getContainer(), "wheel", (e: any) => this.#onMouseWheel(e), this); + } + + setState(state: string) { + console.log("MouseHandler switching state from", this.#state, "to", state); + this.#state = state; + } + + #onMouseDown = (e: LeafletMouseEvent) => { + if (e.originalEvent.button === 0) { + this.leftMousePressed(e); + this.setState(MapMouseHandlerState.LEFT_MOUSE_DOWN); + this.#leftMouseDownEpoch = Date.now(); + this.#leftMouseDownTimeout = window.setTimeout(() => { + this.leftMouseLongClick(e); + this.#leftMouseDownTimeout = null; + }, 300); + } else if (e.originalEvent.button === 1) { + this.mouseWheelPressed(e); + this.setState(MapMouseHandlerState.MOUSE_WHEEL_DOWN); + this.#mouseWheelDownEpoch = Date.now(); + this.#mouseWheelDownTimeout = window.setTimeout(() => { + this.mouseWheelLongClick(e); + this.#mouseWheelDownTimeout = null; + }, 300); + } else if (e.originalEvent.button === 2) { + this.rightMousePressed(e); + this.setState(MapMouseHandlerState.RIGHT_MOUSE_DOWN); + this.#rightMouseDownEpoch = Date.now(); + this.#rightMouseDownTimeout = window.setTimeout(() => { + this.rightMouseLongClick(e); + this.#rightMouseDownTimeout = null; + }, 300); + } + }; + + #onMouseUp = (e: LeafletMouseEvent) => { + if (this.#leftMouseDownTimeout) { + clearTimeout(this.#leftMouseDownTimeout); + this.#leftMouseDownTimeout = null; + } + if (this.#rightMouseDownTimeout) { + clearTimeout(this.#rightMouseDownTimeout); + this.#rightMouseDownTimeout = null; + } + if (this.#rightMouseDownTimeout) { + clearTimeout(this.#rightMouseDownTimeout); + this.#rightMouseDownTimeout = null; + } + + if (this.#state === MapMouseHandlerState.LEFT_MOUSE_DOWN) { + this.leftMouseReleased(e); + if (Date.now() - this.#leftMouseDownEpoch < 300) { + this.setState(MapMouseHandlerState.DEBOUNCING); + this.#debounceTimeout = window.setTimeout(() => { + this.leftMouseShortClick(e); + }, 300); + } + } else if (this.#state === MapMouseHandlerState.MOUSE_WHEEL_DOWN) { + this.mouseWheelReleased(e); + if (Date.now() - this.#mouseWheelDownEpoch < 300) { + this.mouseWheelShortClick(e); + } + } else if (this.#state === MapMouseHandlerState.RIGHT_MOUSE_DOWN) { + this.rightMouseReleased(e); + if (Date.now() - this.#rightMouseDownEpoch < 300) { + this.rightMouseShortClick(e); + } + } + + this.setState(MapMouseHandlerState.IDLE); + }; + + #onDoubleClick = (e: LeafletMouseEvent) => { + this.leftMouseDoubleClick(e); + if (this.#debounceTimeout) { + clearTimeout(this.#debounceTimeout); + } + }; + + #onMouseWheel = (e: LeafletMouseEvent) => { + this.mouseWheel(e); + }; + + #onTouchDown = (e: TouchEvent) => { + let newEvent = { + latlng: this.#map.containerPointToLatLng(this.#map.mouseEventToContainerPoint(e.changedTouches[0] as unknown as MouseEvent)), + originalEvent: e, + } as unknown as LeafletMouseEvent; + + this.leftMousePressed(newEvent); + this.setState(MapMouseHandlerState.LEFT_MOUSE_DOWN); + this.#leftMouseDownEpoch = Date.now(); + this.#leftMouseDownTimeout = window.setTimeout(() => { + this.leftMouseLongClick(newEvent); + this.#leftMouseDownTimeout = null; + }, 300); + }; + + #onTouchUp = (e: TouchEvent) => { + let newEvent = { + latlng: this.#map.containerPointToLatLng(this.#map.mouseEventToContainerPoint(e.changedTouches[0] as unknown as MouseEvent)), + originalEvent: e, + } as unknown as LeafletMouseEvent; + + if (this.#leftMouseDownTimeout) { + clearTimeout(this.#leftMouseDownTimeout); + this.#leftMouseDownTimeout = null; + } + + if (this.#state === MapMouseHandlerState.LEFT_MOUSE_DOWN) { + this.leftMouseReleased(newEvent); + if (Date.now() - this.#leftMouseDownEpoch < 300) { + this.#debounceTimeout = window.setTimeout(() => { + this.leftMouseShortClick(newEvent); + }, 300); + } + } + + this.setState(MapMouseHandlerState.IDLE); + }; + + #onMouseMove = (e: LeafletMouseEvent) => { + this.mouseMove(e); + }; + + #onTouchMove = (e: TouchEvent) => { + let newEvent = { + latlng: this.#map.containerPointToLatLng(this.#map.mouseEventToContainerPoint(e.changedTouches[0] as unknown as MouseEvent)), + originalEvent: e, + } as unknown as LeafletMouseEvent; + + this.mouseMove(newEvent); + }; +} diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 4b39e816..e79e1a5a 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -22,6 +22,10 @@ export function bearing(lat1: number, lon1: number, lat2: number, lon2: number, return brng; } +export function getMagvar(lat: number, lon: number) { + return MagVar.get(lat, lon); +} + export function distance(lat1: number, lon1: number, lat2: number, lon2: number) { const R = 6371e3; // metres const φ1 = deg2rad(lat1); // φ, λ in radians diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index 8516b553..8105002b 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -121,6 +121,7 @@ export class ServerManager { /* If provided, set the credentials */ xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username ?? ""}:${this.#password ?? ""}`)); xmlHttp.setRequestHeader("X-Command-Mode", this.#activeCommandMode); + xmlHttp.timeout = 2000; /* If specified, set the response type */ if (responseType) xmlHttp.responseType = responseType as XMLHttpRequestResponseType; @@ -215,7 +216,7 @@ export class ServerManager { } getUnits(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) { - this.GET(callback, errorCallback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, "arraybuffer", false); + this.GET(callback, errorCallback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, "arraybuffer", refresh); } getWeapons(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) { diff --git a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx index 49576e72..ec28a535 100644 --- a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx @@ -37,12 +37,14 @@ export function MapContextMenu(props: {}) { }); ContextActionSetChangedEvent.on((contextActionSet) => setcontextActionSet(contextActionSet)); MapContextMenuRequestEvent.on((latlng) => { + setUnit(null); setLatLng(latlng); const containerPoint = getApp().getMap().latLngToContainerPoint(latlng); setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y); }); UnitContextMenuRequestEvent.on((unit) => { + setLatLng(null); setUnit(unit); const containerPoint = getApp().getMap().latLngToContainerPoint(unit.getPosition()); setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); @@ -100,8 +102,8 @@ export function MapContextMenu(props: {}) {
diff --git a/frontend/react/src/ui/panels/maptoolbar.tsx b/frontend/react/src/ui/panels/maptoolbar.tsx index 0d2f577b..b71417a2 100644 --- a/frontend/react/src/ui/panels/maptoolbar.tsx +++ b/frontend/react/src/ui/panels/maptoolbar.tsx @@ -140,12 +140,16 @@ export function MapToolBar(props: {}) { {!scrolledTop && ( )} -
onScroll(ev.target)} ref={scrollRef}> +
onScroll(ev.target)} ref={scrollRef}> <>
{ + this.#onLeftLongClick(e); + }, SHORT_PRESS_MILLISECONDS); } else if (e.originalEvent?.button === 2) { if ( getApp().getState() === OlympusState.IDLE || @@ -1617,6 +1620,8 @@ export abstract class Unit extends CustomMarker { DomEvent.preventDefault(e); e.originalEvent.stopImmediatePropagation(); + window.clearTimeout(this.#leftMouseDownTimeout); + if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout); this.#debounceTimeout = window.setTimeout(() => { console.log(`Left short click on ${this.getUnitName()}`); @@ -1631,20 +1636,8 @@ export abstract class Unit extends CustomMarker { }, SHORT_PRESS_MILLISECONDS); } - #onRightShortClick(e: any) { - console.log(`Right short click on ${this.getUnitName()}`); - - window.clearTimeout(this.#rightMouseDownTimeout); - if ( - getApp().getState() === OlympusState.UNIT_CONTROL && - getApp().getMap().getDefaultContextAction() && - getApp().getMap().getDefaultContextAction()?.getTarget() === ContextActionTarget.POINT - ) - getApp().getMap().executeDefaultContextAction(null, this.getPosition(), e.originalEvent); - } - - #onRightLongClick(e: any) { - console.log(`Right long click on ${this.getUnitName()}`); + #onLeftLongClick(e: any) { + console.log(`Left long click on ${this.getUnitName()}`); if (getApp().getState() === OlympusState.IDLE) { this.setSelected(!this.getSelected()); @@ -1669,6 +1662,22 @@ export abstract class Unit extends CustomMarker { } } + #onRightShortClick(e: any) { + console.log(`Right short click on ${this.getUnitName()}`); + + window.clearTimeout(this.#rightMouseDownTimeout); + if ( + getApp().getState() === OlympusState.UNIT_CONTROL && + getApp().getMap().getDefaultContextAction() && + getApp().getMap().getDefaultContextAction()?.getTarget() === ContextActionTarget.POINT + ) + getApp().getMap().executeDefaultContextAction(null, this.getPosition(), e.originalEvent); + } + + #onRightLongClick(e: any) { + console.log(`Right long click on ${this.getUnitName()}`); + } + #onDoubleClick(e: any) { DomEvent.stop(e); DomEvent.preventDefault(e); diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index b019ef2d..4c943b89 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -50,7 +50,6 @@ import { UnitDatabase } from "./databases/unitdatabase"; import * as turf from "@turf/turf"; import { PathMarker } from "../map/markers/pathmarker"; import { Coalition } from "../types/types"; -import { ClusterMarker } from "../map/markers/clustermarker"; /** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only * result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows @@ -340,10 +339,16 @@ export class UnitsManager { pathMarkersCoordinates.forEach((latlng: LatLng) => { if (!this.#pathMarkers.some((pathMarker: PathMarker) => pathMarker.getLatLng().equals(latlng))) { const pathMarker = new PathMarker(latlng); - + pathMarker.on("mousedown", (event) => { + DomEvent.stopPropagation(event); + }); + pathMarker.on("mouseup", (event) => { + DomEvent.stopPropagation(event); + }); pathMarker.on("dragstart", (event) => { event.target.options["freeze"] = true; event.target.options["originalPosition"] = event.target.getLatLng(); + DomEvent.stopPropagation(event); }); pathMarker.on("dragend", (event) => { event.target.options["freeze"] = false; @@ -355,6 +360,7 @@ export class UnitsManager { getApp().getServerManager().addDestination(unit.ID, path); } }); + DomEvent.stopPropagation(event); }); pathMarker.addTo(getApp().getMap());