diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 1368d825..01955e35 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -7,6 +7,9 @@ import { unitTypes } from "../units/unittypes"; import { BoxSelect } from "./boxselect"; import { ContextMenuOption } from "../@types/dom"; +export const IDLE = "IDLE"; +export const MOVE_UNIT = "MOVE_UNIT"; + L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); export interface ClickEvent { @@ -41,7 +44,7 @@ export class Map extends L.Map { this.setLayer("ArcGIS Satellite"); /* Init the state machine */ - this.#state = "IDLE"; + this.#state = IDLE; this.#measurePoint = null; this.#measureIcon = new L.Icon({ iconUrl: 'images/pin.png', iconAnchor: [16, 32]}); @@ -114,10 +117,10 @@ export class Map extends L.Map { /* State machine */ setState(state: string) { this.#state = state; - if (this.#state === "IDLE") { + if (this.#state === IDLE) { L.DomUtil.removeClass(this.getContainer(),'crosshair-cursor-enabled'); } - else if (this.#state === "MOVE_UNIT") { + else if (this.#state === MOVE_UNIT) { L.DomUtil.addClass(this.getContainer(),'crosshair-cursor-enabled'); } document.dispatchEvent(new CustomEvent("mapStateChanged")); @@ -158,7 +161,7 @@ export class Map extends L.Map { #onClick(e: any) { if (!this.#preventLeftClick) { this.hideContextMenu(); - if (this.#state === "IDLE") { + if (this.#state === IDLE) { if (e.originalEvent.ctrlKey) if (!this.#measurePoint) { @@ -173,8 +176,8 @@ export class Map extends L.Map { this.removeLayer(this.#measureMarker); } } - else if (this.#state === "MOVE_UNIT") { - this.setState("IDLE"); + else if (this.#state === MOVE_UNIT) { + this.setState(IDLE); getUnitsManager().deselectAllUnits(); this.hideContextMenu(); } @@ -187,9 +190,9 @@ export class Map extends L.Map { #onContextMenu(e: any) { this.hideContextMenu(); - if (this.#state === "IDLE") { + if (this.#state === IDLE) { var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: null, coalitionID: null}; - if (this.#state == "IDLE") { + if (this.#state == IDLE) { var options = [ { "tooltip": "Spawn air unit", "src": "spawnAir.png", "callback": () => this.#aircraftSpawnMenu(spawnEvent) }, { "tooltip": "Spawn ground unit", "src": "spawnGround.png", "callback": () => this.#groundUnitSpawnMenu(spawnEvent) }, @@ -199,7 +202,7 @@ export class Map extends L.Map { this.showContextMenu(spawnEvent, "Action", options, false); } } - else if (this.#state === "MOVE_UNIT") { + else if (this.#state === MOVE_UNIT) { if (!e.originalEvent.ctrlKey) { getUnitsManager().selectedUnitsClearDestinations(); } diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index 6ffcc274..05d70d0f 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -3,12 +3,12 @@ import { Slider } from "../controls/slider"; import { Aircraft, AirUnit, GroundUnit, Helicopter, NavyUnit, Unit } from "../units/unit"; import { Panel } from "./panel"; -var ROEs: string[] = ["Free", "Designated free", "Designated", "Return", "Hold"]; -var reactionsToThreat: string[] = [ "None", "Passive", "Evade", "Escape", "Abort"]; -var minSpeedValues: {[key: string]: number} = {Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0}; -var maxSpeedValues: {[key: string]: number} = {Aircraft: 800, Helicopter: 300, NavyUnit: 60, GroundUnit: 60}; -var minAltitudeValues: {[key: string]: number} = {Aircraft: 500, Helicopter: 0, NavyUnit: 0, GroundUnit: 0}; -var maxAltitudeValues: {[key: string]: number} = {Aircraft: 50000, Helicopter: 10000, NavyUnit: 60, GroundUnit: 60}; +const ROEs: string[] = ["Free", "Designated free", "Designated", "Return", "Hold"]; +const reactionsToThreat: string[] = [ "None", "Passive", "Evade", "Escape", "Abort"]; +const minSpeedValues: {[key: string]: number} = {Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0}; +const maxSpeedValues: {[key: string]: number} = {Aircraft: 800, Helicopter: 300, NavyUnit: 60, GroundUnit: 60}; +const minAltitudeValues: {[key: string]: number} = {Aircraft: 500, Helicopter: 0, NavyUnit: 0, GroundUnit: 0}; +const maxAltitudeValues: {[key: string]: number} = {Aircraft: 50000, Helicopter: 10000, NavyUnit: 60, GroundUnit: 60}; export class UnitControlPanel extends Panel { #altitudeSlider: Slider; @@ -40,6 +40,9 @@ export class UnitControlPanel extends Panel { this.getElement().querySelector("#roe-buttons-container")?.append(...this.#optionButtons["ROE"]); this.getElement().querySelector("#reaction-to-threat-buttons-container")?.append(...this.#optionButtons["reactionToThreat"]); + document.addEventListener("unitsSelection", (e: CustomEvent) => {console.log("Select"); this.show(); this.update(e.detail)}); + document.addEventListener("clearSelection", () => {this.hide()}); + this.hide(); } diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 5659cb23..e69a68d2 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -49,7 +49,7 @@ export function getUnits(callback: CallableFunction, refresh: boolean = false) { if (!DEMO) GET(callback, `${UNITS_URI}/${refresh? REFRESH_URI: UPDATE_URI}}`); else - callback(DEMO_UNITS_DATA); + callback(refresh? generateRandomUnitsDemoData(1000): {units:{}}); } export function addDestination(ID: number, path: any) { @@ -142,91 +142,57 @@ export function setReactionToThreat(ID: number, reactionToThreat: string) { POST(data, () => { }); } -const DEMO_UNITS_DATA = { - units: { - "1": { - AI: true, - name: "F-5E", - unitName: "Olympus 1-1", - groupName: "Group 1", - alive: true, - category: "Aircraft", - flightData: { - latitude: 37.3, - longitude: -116, - altitude: 2000, - heading: 0.5, - speed: 300 - }, - missionData: { - fuel: 0.5, - flags: {human: false}, - ammo: [], - targets: [], - hasTask: true, - coalition: "blue" - }, - formationData: { - formation: "Echelon", - isLeader: false, - isWingman: false, - leaderID: null, - wingmen: [], - wingmenIDs: [] - }, - taskData: { - currentTask: "Example task", - activePath: undefined, - targetSpeed: 400, - targetAltitude: 3000 - }, - optionsData: { - ROE: "None", - reactionToThreat: "None", - } - }, - "2": { - AI: true, - name: "F-5E", - unitName: "Olympus 1-1", - groupName: "Group 1", - alive: true, - category: "Aircraft", - flightData: { - latitude: 37.3, - longitude: -115.9, - altitude: 2000, - heading: .5, - speed: 300 - }, - missionData: { - fuel: 0.5, - flags: {human: false}, - ammo: [], - targets: [], - hasTask: true, - coalition: "red" - }, - formationData: { - formation: "Echelon", - isLeader: false, - isWingman: false, - leaderID: null, - wingmen: [], - wingmenIDs: [] - }, - taskData: { - currentTask: "Example task", - activePath: undefined, - targetSpeed: 400, - targetAltitude: 3000 - }, - optionsData: { - ROE: "None", - reactionToThreat: "None", - } - } + +function generateRandomUnitsDemoData(unitsNumber: number) +{ + var units: any = {}; + for (let i = 0; i < unitsNumber; i++) + { + units[String(i)] = structuredClone(DEMO_UNIT_DATA); + units[String(i)].flightData.latitude += (Math.random() - 0.5) * 0.1; + units[String(i)].flightData.longitude += (Math.random() - 0.5) * 0.1; + } + return {"units": units}; +} + +const DEMO_UNIT_DATA = { + AI: true, + name: "F-5E", + unitName: "Olympus 1-1", + groupName: "Group 1", + alive: true, + category: "Aircraft", + flightData: { + latitude: 37.3, + longitude: -116, + altitude: 2000, + heading: 0.5, + speed: 300 }, - bullseyes: [], - airbases: [] + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "blue" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + targetSpeed: 400, + targetAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } } \ No newline at end of file diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 32c38aa8..5b0549f9 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -2,10 +2,12 @@ import { LatLng, LatLngBounds } from "leaflet"; import { getMap, getUnitControlPanel, getUnitInfoPanel } from ".."; import { Unit, GroundUnit } from "./unit"; import { cloneUnit } from "../server/server"; +import { IDLE, MOVE_UNIT } from "../map/map"; export class UnitsManager { #units: { [ID: number]: Unit }; #copiedUnits: Unit[]; + #selectionEventDisabled: boolean = false; constructor() { this.#units = {}; @@ -13,7 +15,7 @@ export class UnitsManager { document.addEventListener('copy', () => this.copyUnits()); document.addEventListener('paste', () => this.pasteUnits()); - document.addEventListener('unitSelection', () => this.onUnitSelection()); + document.addEventListener('unitSelection', (e: CustomEvent) => this.onUnitSelection(e.detail)); document.addEventListener('keydown', (event) => this.#onKeyDown(event)); } @@ -41,13 +43,19 @@ export class UnitsManager { } update(data: UnitsData) { - for (let ID in data.units) { - /* Create the unit if missing from the local array, then update the data. Drawing is handled by leaflet. */ - if (!(ID in this.#units)) { - this.addUnit(parseInt(ID), data.units[ID]); - } - this.#units[parseInt(ID)].setData(data.units[ID]); - } + Object.keys(data.units) + .filter((ID: string) => !(ID in this.#units)) + .reduce((timeout: number, ID: string) => { + setTimeout(() => { + this.addUnit(parseInt(ID), data.units[ID]); + this.#units[parseInt(ID)].setData(data.units[ID]); + }, timeout); + return timeout + 10; + }, 10); + + Object.keys(data.units) + .filter((ID: string) => ID in this.#units) + .forEach((ID: string) => this.#units[parseInt(ID)].setData(data.units[ID])); /* Update the unit info panel */ if (this.getSelectedUnits().length == 1) { @@ -67,21 +75,28 @@ export class UnitsManager { selectUnit(ID: number, deselectAllUnits: boolean = true) { - if (deselectAllUnits) - this.deselectAllUnits(); + if (deselectAllUnits) + this.getSelectedUnits().filter((unit: Unit) => unit.ID !== ID ).forEach((unit: Unit) => unit.setSelected(false)); this.#units[ID]?.setSelected(true); } - onUnitSelection() { + onUnitSelection(unit: Unit) { if (this.getSelectedUnits().length > 0) { - getMap().setState("MOVE_UNIT"); - document.dispatchEvent(new CustomEvent("unitsSelection", {detail: this.getSelectedUnits()})); + getMap().setState(MOVE_UNIT); + /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ + if (!this.#selectionEventDisabled) + { + setTimeout(() => { + document.dispatchEvent(new CustomEvent("unitsSelection", {detail: this.getSelectedUnits()})); + this.#selectionEventDisabled = false; + }, 300); + this.#selectionEventDisabled = true; + } } else { - getMap().setState("IDLE"); + getMap().setState(IDLE); document.dispatchEvent(new CustomEvent("clearSelection")); } - this.#updateUnitControlPanel(); } selectFromBounds(bounds: LatLngBounds) @@ -147,7 +162,7 @@ export class UnitsManager { getSelectedUnitsType () { return this.getSelectedUnits().map((unit: Unit) => { return unit.constructor.name - }).reduce((a: any, b: any) => { + })?.reduce((a: any, b: any) => { return a == b? a: undefined }); }; @@ -155,7 +170,7 @@ export class UnitsManager { getSelectedUnitsTargetSpeed () { return this.getSelectedUnits().map((unit: Unit) => { return unit.getTaskData().targetSpeed - }).reduce((a: any, b: any) => { + })?.reduce((a: any, b: any) => { return a == b? a: undefined }); }; @@ -163,7 +178,7 @@ export class UnitsManager { getSelectedUnitsTargetAltitude () { return this.getSelectedUnits().map((unit: Unit) => { return unit.getTaskData().targetAltitude - }).reduce((a: any, b: any) => { + })?.reduce((a: any, b: any) => { return a == b? a: undefined }); }; @@ -200,8 +215,6 @@ export class UnitsManager { { selectedUnits[idx].changeSpeed(speedChange); } - - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail } selectedUnitsChangeAltitude(altitudeChange: string) @@ -211,8 +224,6 @@ export class UnitsManager { { selectedUnits[idx].changeAltitude(altitudeChange); } - - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail } selectedUnitsSetSpeed(speed: number) @@ -240,8 +251,6 @@ export class UnitsManager { { selectedUnits[idx].setROE(ROE); } - - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail } selectedUnitsSetReactionToThreat(reactionToThreat: string) @@ -251,8 +260,6 @@ export class UnitsManager { { selectedUnits[idx].setReactionToThreat(reactionToThreat); } - - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail } selectedUnitsAttackUnit(ID: number) { @@ -312,16 +319,14 @@ export class UnitsManager { //console.log("At least 2 units must be selected to create a formation."); } } - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail } - selectedUnitsUndoFormation(ID: number | null = null) + selectedUnitsUndoFormation() { for (let leader of this.getSelectedLeaders()) { leader.setLeader(false); } - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail } selectedUnitsDelete() @@ -347,17 +352,6 @@ export class UnitsManager { } } - #updateUnitControlPanel() { - /* Update the unit control panel */ - if (this.getSelectedUnits().length > 0) { - getUnitControlPanel()?.show(); - getUnitControlPanel()?.update(this.getSelectedLeaders().concat(this.getSelectedSingletons())); - } - else { - getUnitControlPanel()?.hide(); - } - } - #onKeyDown(event: KeyboardEvent) { if (event.key === "Delete")