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