feat: Control scheme improvement

This commit is contained in:
Davide Passoni 2025-03-24 16:55:40 +01:00
parent 0b8fb969b2
commit eff96ce0c2
11 changed files with 554 additions and 286 deletions

View File

@ -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();
},

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {}) {
<div
ref={contentRef}
className={`
absolute flex min-w-80 gap-2 rounded-md bg-olympus-600
`}
absolute flex min-w-80 gap-2 rounded-md bg-olympus-600
`}
>
<div
className={`

View File

@ -91,14 +91,14 @@ export function ControlsPanel(props: {}) {
target: faFighterJet,
text: "Show unit actions",
});
controls.push({
actions: shortcuts["toggleRelativePositions"]?.toActions(),
text: "Activate group movement",
});
controls.push({
actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
text: "Rotate formation",
});
//controls.push({
// actions: shortcuts["toggleRelativePositions"]?.toActions(),
// text: "Activate group movement",
//});
//controls.push({
// actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
// text: "Rotate formation",
//});
} else if (appState === OlympusState.SPAWN) {
controls = [
{

View File

@ -21,9 +21,10 @@ export function InfoBar(props: {}) {
<div
key={idx}
className={`
absolute w-fit translate-x-[-50%] gap-2 text-nowrap rounded-full
absolute w-[250px] translate-x-[-50%] gap-2 rounded-full
bg-olympus-800/90 px-4 py-2 text-center text-sm text-white
shadow-md backdrop-blur-lg backdrop-grayscale
sm:w-fit sm:text-nowrap
`}
style={{ top: `${idx * 20}px` }}
>

View File

@ -140,12 +140,16 @@ export function MapToolBar(props: {}) {
{!scrolledTop && (
<FaChevronUp
className={`
absolute top-0 h-6 w-full rounded-lg px-3.5 py-1 text-gray-200
absolute top-0 h-6 w-full rounded-lg bg-red-500 px-3.5 py-1
text-gray-200
dark:bg-olympus-900
`}
/>
)}
<div className={`flex flex-col gap-2 overflow-y-auto no-scrollbar p-2`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
<div className={`
pointer-events-auto flex flex-col gap-2 overflow-y-auto no-scrollbar
p-2
`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
<>
<div className="flex flex-col gap-1">
<OlStateButton

View File

@ -1593,6 +1593,9 @@ export abstract class Unit extends CustomMarker {
this.#isLeftMouseDown = true;
this.#leftMouseDownEpoch = Date.now();
this.#leftMouseDownTimeout = window.setTimeout(() => {
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);

View File

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