From 89c39c70382edfd847627c72f92dbc531ba140af Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Fri, 8 Sep 2023 17:40:53 +0200 Subject: [PATCH] Code documentation and refactoring --- client/src/@types/dom.d.ts | 5 ++ client/src/index.ts | 123 ++++++++++++--------------- client/src/mission/missionhandler.ts | 2 +- client/src/panels/panel.ts | 2 +- client/src/panels/unitinfopanel.ts | 30 ++----- client/src/unit/unitsmanager.ts | 58 ++++++++++--- client/views/toolbars/primary.ejs | 10 --- 7 files changed, 115 insertions(+), 115 deletions(-) diff --git a/client/src/@types/dom.d.ts b/client/src/@types/dom.d.ts index dc70c183..7fea9ff0 100644 --- a/client/src/@types/dom.d.ts +++ b/client/src/@types/dom.d.ts @@ -31,6 +31,11 @@ declare global { } } +export interface ConfigParameters { + port: number; + address: string; +} + export interface ContextMenuOption { tooltip: string; src: string; diff --git a/client/src/index.ts b/client/src/index.ts index 0fbf906f..1802e300 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -2,33 +2,32 @@ import { Map } from "./map/map" import { UnitsManager } from "./unit/unitsmanager"; import { UnitInfoPanel } from "./panels/unitinfopanel"; import { ConnectionStatusPanel } from "./panels/connectionstatuspanel"; -import { MissionHandler } from "./mission/missionhandler"; +import { MissionManager } from "./mission/missionhandler"; import { UnitControlPanel } from "./panels/unitcontrolpanel"; import { MouseInfoPanel } from "./panels/mouseinfopanel"; -import { AIC } from "./aic/aic"; -import { ATC } from "./atc/atc"; -import { FeatureSwitches } from "./features/featureswitches"; import { LogPanel } from "./panels/logpanel"; import { getConfig, getPaused, setAddress, setCredentials, setPaused, startUpdate, toggleDemoEnabled } from "./server/server"; -import { UnitDataTable } from "./atc/unitdatatable"; import { keyEventWasInInput } from "./other/utils"; import { Popup } from "./popups/popup"; -import { Dropdown } from "./controls/dropdown"; import { HotgroupPanel } from "./panels/hotgrouppanel"; import { SVGInjector } from "@tanem/svg-injector"; import { BLUE_COMMANDER, GAME_MASTER, RED_COMMANDER } from "./constants/constants"; import { ServerStatusPanel } from "./panels/serverstatuspanel"; import { WeaponsManager } from "./weapon/weaponsmanager"; +import { ConfigParameters } from "./@types/dom"; +/* Global data */ +var activeCoalition: string = "blue"; + +/* Main leaflet map, extended by custom methods */ var map: Map; +/* Managers */ var unitsManager: UnitsManager; var weaponsManager: WeaponsManager; -var missionHandler: MissionHandler; - -var aic: AIC; -var atc: ATC; +var missionHandler: MissionManager; +/* UI Panels */ var unitInfoPanel: UnitInfoPanel; var connectionStatusPanel: ConnectionStatusPanel; var serverStatusPanel: ServerStatusPanel; @@ -37,22 +36,16 @@ var mouseInfoPanel: MouseInfoPanel; var logPanel: LogPanel; var hotgroupPanel: HotgroupPanel; +/* Popups */ var infoPopup: Popup; -var activeCoalition: string = "blue"; - -var unitDataTable: UnitDataTable; - -var featureSwitches; - function setup() { - featureSwitches = new FeatureSwitches(); - /* Initialize base functionalitites */ + map = new Map('map-container'); + unitsManager = new UnitsManager(); weaponsManager = new WeaponsManager(); - map = new Map('map-container'); - missionHandler = new MissionHandler(); + missionHandler = new MissionManager(); /* Panels */ unitInfoPanel = new UnitInfoPanel("unit-info-panel"); @@ -66,36 +59,21 @@ function setup() { /* Popups */ infoPopup = new Popup("info-popup"); - /* Controls */ - new Dropdown("app-icon", () => { }); - - /* Unit data table */ - unitDataTable = new UnitDataTable("unit-data-table"); - - /* AIC */ - let aicFeatureSwitch = featureSwitches.getSwitch("aic"); - if (aicFeatureSwitch?.isEnabled()) { - aic = new AIC(); - } - - /* ATC */ - let atcFeatureSwitch = featureSwitches.getSwitch("atc"); - if (atcFeatureSwitch?.isEnabled()) { - atc = new ATC(); - atc.startUpdates(); - } - /* Setup event handlers */ setupEvents(); - /* Load the config file */ - getConfig(readConfig); + /* Load the config file from the app server*/ + getConfig((config: ConfigParameters) => readConfig(config)); } -function readConfig(config: any) { - if (config && config["address"] != undefined && config["port"] != undefined) { - const address = config["address"]; - const port = config["port"]; +/** Loads the configuration parameters + * + * @param config ConfigParameters, defines the address and port of the Olympus REST server + */ +function readConfig(config: ConfigParameters) { + if (config && config.address != undefined && config.port != undefined) { + const address = config.address; + const port = config.port; if (typeof address === 'string' && typeof port == 'number') setAddress(address == "*" ? window.location.hostname : address, port); } @@ -104,9 +82,14 @@ function readConfig(config: any) { } } -function setupEvents() { - /* Generic clicks */ +/** Setup the global window events + * + */ +function setupEvents() { + /* Generic clicks. The "data-on-click" html parameter is used to call a generic callback from the html code. + It is used by all the statically defined elements of the UI. Dynamically generated elements should directly generate the events instead. + */ document.addEventListener("click", (ev) => { if (ev instanceof MouseEvent && ev.target instanceof HTMLElement) { const target = ev.target; @@ -140,9 +123,6 @@ function setupEvents() { case "KeyT": toggleDemoEnabled(); break; - case "Quote": - unitDataTable.toggle(); - break case "Space": setPaused(!getPaused()); break; @@ -151,7 +131,7 @@ function setupEvents() { getMap().handleMapPanning(ev); break; case "Digit1": case "Digit2": case "Digit3": case "Digit4": case "Digit5": case "Digit6": case "Digit7": case "Digit8": case "Digit9": - // Using the substring because the key will be invalid when pressing the Shift key + /* Using the substring because the key will be invalid when pressing the Shift key */ if (ev.ctrlKey && ev.shiftKey) getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5))); else if (ev.ctrlKey && !ev.shiftKey) @@ -174,20 +154,18 @@ function setupEvents() { } }); + // TODO: move from here in dedicated class document.addEventListener("closeDialog", (ev: CustomEventInit) => { ev.detail._element.closest(".ol-dialog").classList.add("hide"); }); - document.addEventListener("toggleElements", (ev: CustomEventInit) => { - document.querySelectorAll(ev.detail.selector).forEach(el => { - el.classList.toggle("hide"); - }) - }); - + /* Try and connect with the Olympus REST server */ document.addEventListener("tryConnection", () => { const form = document.querySelector("#splash-content")?.querySelector("#authentication-form"); - const username = ((form?.querySelector("#username"))).value; - const password = ((form?.querySelector("#password"))).value; + const username = (form?.querySelector("#username") as HTMLInputElement).value; + const password = (form?.querySelector("#password") as HTMLInputElement).value; + + /* Update the user credentials */ setCredentials(username, password); /* Start periodically requesting updates */ @@ -196,10 +174,12 @@ function setupEvents() { setLoginStatus("connecting"); }) + /* Reload the page, used to mimic a restart of the app */ document.addEventListener("reloadPage", () => { location.reload(); }) + /* Inject the svgs with the corresponding svg code. This allows to dynamically manipulate the svg, like changing colors */ document.querySelectorAll("[inject-svg]").forEach((el: Element) => { var img = el as HTMLImageElement; var isLoaded = img.complete; @@ -212,14 +192,11 @@ function setupEvents() { }) } +/* Getters */ export function getMap() { return map; } -export function getUnitDataTable() { - return unitDataTable; -} - export function getUnitsManager() { return unitsManager; } @@ -260,11 +237,23 @@ export function getHotgroupPanel() { return hotgroupPanel; } +export function getInfoPopup() { + return infoPopup; +} + +/** Set the active coalition, i.e. the currently controlled coalition. A game master can change the active coalition, while a commander is bound to his/her coalition + * + * @param newActiveCoalition + */ export function setActiveCoalition(newActiveCoalition: string) { if (getMissionHandler().getCommandModeOptions().commandMode == GAME_MASTER) activeCoalition = newActiveCoalition; } +/** + * + * @returns The active coalition + */ export function getActiveCoalition() { if (getMissionHandler().getCommandModeOptions().commandMode == GAME_MASTER) return activeCoalition; @@ -278,15 +267,15 @@ export function getActiveCoalition() { } } +/** Set a message in the login splash screen + * + * @param status The message to show in the login splash screen + */ export function setLoginStatus(status: string) { const el = document.querySelector("#login-status") as HTMLElement; if (el) el.dataset["status"] = status; } -export function getInfoPopup() { - return infoPopup; -} - window.onload = setup; diff --git a/client/src/mission/missionhandler.ts b/client/src/mission/missionhandler.ts index 4edf479d..ed44828b 100644 --- a/client/src/mission/missionhandler.ts +++ b/client/src/mission/missionhandler.ts @@ -11,7 +11,7 @@ import { aircraftDatabase } from "../unit/aircraftdatabase"; import { helicopterDatabase } from "../unit/helicopterdatabase"; import { navyUnitDatabase } from "../unit/navyunitdatabase"; -export class MissionHandler { +export class MissionManager { #bullseyes: { [name: string]: Bullseye } = {}; #airbases: { [name: string]: Airbase } = {}; #theatre: string = ""; diff --git a/client/src/panels/panel.ts b/client/src/panels/panel.ts index 204f19ac..c4bec3b5 100644 --- a/client/src/panels/panel.ts +++ b/client/src/panels/panel.ts @@ -7,7 +7,7 @@ export class Panel { * @param ID - the ID of the HTML element which will contain the context menu */ constructor(ID: string){ - this.#element = document.getElementById(ID); + this.#element = document.getElementById(ID) as HTMLElement; } show() { diff --git a/client/src/panels/unitinfopanel.ts b/client/src/panels/unitinfopanel.ts index d63e9aea..b4367d75 100644 --- a/client/src/panels/unitinfopanel.ts +++ b/client/src/panels/unitinfopanel.ts @@ -1,21 +1,12 @@ -import { getUnitsManager } from ".."; import { Ammo } from "../@types/unit"; -import { ConvertDDToDMS, rad2deg } from "../other/utils"; import { aircraftDatabase } from "../unit/aircraftdatabase"; import { Unit } from "../unit/unit"; import { Panel } from "./panel"; export class UnitInfoPanel extends Panel { - #altitude: HTMLElement; #currentTask: HTMLElement; #fuelBar: HTMLElement; #fuelPercentage: HTMLElement; - #groundSpeed: HTMLElement; - #groupName: HTMLElement; - #heading: HTMLElement; - #name: HTMLElement; - #latitude: HTMLElement; - #longitude: HTMLElement; #loadoutContainer: HTMLElement; #silhouette: HTMLImageElement; #unitControl: HTMLElement; @@ -29,17 +20,10 @@ export class UnitInfoPanel extends Panel { constructor(ID: string){ super(ID); - this.#altitude = (this.getElement().querySelector("#altitude")) as HTMLElement; this.#currentTask = (this.getElement().querySelector("#current-task")) as HTMLElement; - this.#groundSpeed = (this.getElement().querySelector("#ground-speed")) as HTMLElement; this.#fuelBar = (this.getElement().querySelector("#fuel-bar")) as HTMLElement; this.#fuelPercentage = (this.getElement().querySelector("#fuel-percentage")) as HTMLElement; - this.#groupName = (this.getElement().querySelector("#group-name")) as HTMLElement; - this.#heading = (this.getElement().querySelector("#heading")) as HTMLElement; - this.#name = (this.getElement().querySelector("#name")) as HTMLElement; - this.#latitude = (this.getElement().querySelector("#latitude")) as HTMLElement; this.#loadoutContainer = (this.getElement().querySelector("#loadout-container")) as HTMLElement; - this.#longitude = (this.getElement().querySelector("#longitude")) as HTMLElement; this.#silhouette = (this.getElement().querySelector("#loadout-silhouette")) as HTMLImageElement; this.#unitControl = (this.getElement().querySelector("#unit-control")) as HTMLElement; this.#unitLabel = (this.getElement().querySelector("#unit-label")) as HTMLElement; @@ -56,14 +40,12 @@ export class UnitInfoPanel extends Panel { #onUnitUpdate(unit: Unit) { if (this.getElement() != null && this.getVisible() && unit.getSelected()) { - const baseData = unit.getData(); - /* Set the unit info */ - this.#unitLabel.innerText = aircraftDatabase.getByName(baseData.name)?.label || baseData.name; - this.#unitName.innerText = baseData.unitName; + this.#unitLabel.innerText = aircraftDatabase.getByName(unit.getName())?.label || unit.getName(); + this.#unitName.innerText = unit.getUnitName(); if (unit.getHuman()) this.#unitControl.innerText = "Human"; - else if (baseData.controlled) + else if (unit.getControlled()) this.#unitControl.innerText = "Olympus controlled"; else this.#unitControl.innerText = "DCS Controlled"; @@ -72,11 +54,11 @@ export class UnitInfoPanel extends Panel { this.#currentTask.dataset.currentTask = unit.getTask() !== "" ? unit.getTask() : "No task"; this.#currentTask.dataset.coalition = unit.getCoalition(); - this.#silhouette.src = `/images/units/${unit.getDatabase()?.getByName(baseData.name)?.filename}`; - this.#silhouette.classList.toggle("hide", unit.getDatabase()?.getByName(baseData.name)?.filename == undefined || unit.getDatabase()?.getByName(baseData.name)?.filename == ''); + this.#silhouette.src = `/images/units/${unit.getDatabase()?.getByName(unit.getName())?.filename}`; + this.#silhouette.classList.toggle("hide", unit.getDatabase()?.getByName(unit.getName())?.filename == undefined || unit.getDatabase()?.getByName(unit.getName())?.filename == ''); /* Add the loadout elements */ - const items = this.#loadoutContainer.querySelector("#loadout-items"); + const items = this.#loadoutContainer.querySelector("#loadout-items") as HTMLElement; if (items) { const ammo = Object.values(unit.getAmmo()); diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index 175f374f..10d16abe 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -5,7 +5,7 @@ import { cloneUnits, deleteUnit, spawnAircrafts, spawnGroundUnits, spawnHelicopt import { bearingAndDistanceToLatLng, deg2rad, getUnitDatabaseByCategory, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils"; import { CoalitionArea } from "../map/coalitionarea"; import { groundUnitDatabase } from "./groundunitdatabase"; -import { DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT, NONE } from "../constants/constants"; +import { DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { Contact, UnitData } from "../@types/unit"; import { citiesDatabase } from "./citiesDatabase"; @@ -14,6 +14,10 @@ import { helicopterDatabase } from "./helicopterdatabase"; import { navyUnitDatabase } from "./navyunitdatabase"; import { TemporaryUnitMarker } from "../map/temporaryunitmarker"; +/** 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 + * to avoid client/server and client/client inconsistencies. + */ export class UnitsManager { #units: { [ID: number]: Unit }; #copiedUnits: UnitData[]; @@ -40,20 +44,19 @@ export class UnitsManager { document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.selectedUnitsChangeAltitude(e.detail.type) }); } - getSelectableAircraft() { - const units = this.getUnits(); - return Object.keys(units).reduce((acc: { [key: number]: Unit }, unitId: any) => { - if (units[unitId].getCategory() === "Aircraft" && units[unitId].getAlive() === true) { - acc[unitId] = units[unitId]; - } - return acc; - }, {}); - } - + /** + * + * @returns All the existing units, both alive and dead + */ getUnits() { return this.#units; } + /** Get a specific unit by ID + * + * @param ID ID of the unit. The ID shall be the same as the unit ID in DCS. + * @returns Unit object, or null if no unit with said ID exists. + */ getUnitByID(ID: number) { if (ID in this.#units) return this.#units[ID]; @@ -61,13 +64,23 @@ export class UnitsManager { return null; } + /** Returns all the units that belong to a hotgroup + * + * @param hotgroup Hotgroup number + * @returns Array of units that belong to hotgroup + */ getUnitsByHotgroup(hotgroup: number) { return Object.values(this.#units).filter((unit: Unit) => { return unit.getAlive() && unit.getHotgroup() == hotgroup }); } + /** Add a new unit to the manager + * + * @param ID ID of the new unit + * @param category Either "Aircraft", "Helicopter", "GroundUnit", or "NavyUnit". Determines what class will be used to create the new unit accordingly. + */ addUnit(ID: number, category: string) { if (category) { - /* The name of the unit category is exactly the same as the constructor name */ + /* Get the constructor from the unit category */ var constructor = Unit.getConstructor(category); if (constructor != undefined) { this.#units[ID] = new constructor(ID); @@ -75,11 +88,25 @@ export class UnitsManager { } } + /** Update the data of all the units. The data is directly decoded from the binary buffer received from the REST Server. This is necessary for performance and bandwidth reasons. + * + * @param buffer The arraybuffer, encoded according to the ICD defined in: TODO Add reference to ICD + * @returns The decoded updateTime of the data update. + */ update(buffer: ArrayBuffer) { + /* Extract the data from the arraybuffer. Since data is encoded dynamically (not all data is always present, but rather only the data that was actually updated since the last request). + No a prori casting can be performed. On the contrary, the array is decoded incrementally, depending on the DataIndexes of the data. The actual data decoding is performed by the Unit class directly. + Every time a piece of data is decoded the decoder seeker is incremented. */ var dataExtractor = new DataExtractor(buffer); + var updateTime = Number(dataExtractor.extractUInt64()); + + /* Run until all data is extracted or an error occurs */ while (dataExtractor.getSeekPosition() < buffer.byteLength) { + /* Extract the unit ID */ const ID = dataExtractor.extractUInt32(); + + /* If the ID of the unit does not yet exist, create the unit, if the category is known. If it isn't, some data must have been lost and we need to wait for another update */ if (!(ID in this.#units)) { const datumIndex = dataExtractor.extractUInt8(); if (datumIndex == DataIndexes.category) { @@ -91,9 +118,13 @@ export class UnitsManager { return updateTime; } } + /* Update the data of the unit */ this.#units[ID]?.setData(dataExtractor); } + /* If we are not in Game Master mode, visibility of units by the user is determined by the detections of the units themselves. This is performed here. + This operation is computationally expensive, therefore it is only performed when #requestDetectionUpdate is true. This happens whenever a change in the detectionUpdates is detected + */ if (this.#requestDetectionUpdate && getMissionHandler().getCommandModeOptions().commandMode != GAME_MASTER) { /* Create a dictionary of empty detection methods arrays */ var detectionMethods: { [key: string]: number[] } = {}; @@ -120,6 +151,8 @@ export class UnitsManager { const unit = this.#units[ID]; unit?.setDetectionMethods(detectionMethods[ID]); } + + /* Set the detection methods for every weapon (weapons must be detected too) */ for (let ID in getWeaponsManager().getWeapons()) { const weapon = getWeaponsManager().getWeaponByID(parseInt(ID)); weapon?.setDetectionMethods(detectionMethods[ID]); @@ -128,6 +161,7 @@ export class UnitsManager { this.#requestDetectionUpdate = false; } + /* Update the detection lines of all the units. This code is handled by the UnitsManager since it must be run both when the detected OR the detecting unit is updated */ for (let ID in this.#units) { if (this.#units[ID].getSelected()) this.#units[ID].drawLines(); diff --git a/client/views/toolbars/primary.ejs b/client/views/toolbars/primary.ejs index a687a34b..708739e2 100644 --- a/client/views/toolbars/primary.ejs +++ b/client/views/toolbars/primary.ejs @@ -62,14 +62,4 @@ data-on-click-params='{ "coalition": "neutral" }'>NEUTRAL - - \ No newline at end of file