diff --git a/client/demo.js b/client/demo.js index 6c7c354b..04522530 100644 --- a/client/demo.js +++ b/client/demo.js @@ -14,7 +14,7 @@ const DEMO_UNIT_DATA = { TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 }, generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false }, - ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], + ammo: [{ quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } ], contacts: [{ID: 2, detectionMethod: 1}, {ID: 3, detectionMethod: 4}], activePath: [{lat: 38, lng: -115, alt: 0}, {lat: 38, lng: -114, alt: 0}] }, @@ -63,6 +63,38 @@ const DEMO_UNIT_DATA = { ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], contacts: [{ID: 1, detectionMethod: 16}], activePath: [ ] + }, ["5"]:{ category: "GroundUnit", alive: true, human: false, controlled: true, coalition: 1, country: 0, name: "Gepard", unitName: "Cool guy 2-1", groupName: "Cool group 4", state: 1, task: "Being cool", + hasTask: false, position: { lat: 37.2, lng: -116.1, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50, + desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0, + formationOffset: { x: 0, y: 0, z: 0 }, + targetID: 0, + targetPosition: { lat: 0, lng: 0, alt: 0 }, + ROE: 2, + reactionToThreat: 1, + emissionsCountermeasures: 1, + TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, + radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 }, + generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false }, + ammo: [{ quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } ], + contacts: [{ID: 1, detectionMethod: 16}], + activePath: [ ], + isLeader: true + }, ["6"]:{ category: "GroundUnit", alive: true, human: false, controlled: true, coalition: 1, country: 0, name: "Gepard", unitName: "Cool guy 2-2", groupName: "Cool group 4", state: 1, task: "Being cool", + hasTask: false, position: { lat: 37.21, lng: -116.1, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50, + desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0, + formationOffset: { x: 0, y: 0, z: 0 }, + targetID: 0, + targetPosition: { lat: 0, lng: 0, alt: 0 }, + ROE: 2, + reactionToThreat: 1, + emissionsCountermeasures: 1, + TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, + radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 }, + generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false }, + ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ], + contacts: [{ID: 1, detectionMethod: 16}], + activePath: [ ], + isLeader: false } } @@ -128,6 +160,7 @@ class DemoDataGenerator { array = this.appendAmmo(array, unit.ammo, 35); array = this.appendContacts(array, unit.contacts, 36); array = this.appendActivePath(array, unit.activePath, 37); + array = this.appendUint8(array, unit.isLeader, 38); array = this.concat(array, this.uint8ToByteArray(255)); } res.end(Buffer.from(array, 'binary')); @@ -290,8 +323,10 @@ class DemoDataGenerator { } logs(req, res){ - var ret = {logs: {}}; + var ret = {logs: {"1": "I'm a log!", "2": "I'm a different log!"}}; ret.time = Date.now(); + ret.frameRate = 60; + ret.load = 0; res.send(JSON.stringify(ret)); }; diff --git a/client/public/stylesheets/layout/layout.css b/client/public/stylesheets/layout/layout.css index 8e139aac..4bcced84 100644 --- a/client/public/stylesheets/layout/layout.css +++ b/client/public/stylesheets/layout/layout.css @@ -49,7 +49,16 @@ font-size: 12px; position: absolute; right: 10px; - width: 250px; + width: 180px; + z-index: 9999; +} + +#server-status-panel { + bottom: 20px; + font-size: 12px; + position: absolute; + right: 200px; + width: 300px; z-index: 9999; } diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 365029de..15f6e73d 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -3,6 +3,7 @@ @import url("atc/unitdatatable.css"); @import url("aic/aic.css"); @import url("panels/connectionstatus.css"); +@import url("panels/serverstatus.css"); @import url("panels/mouseinfo.css"); @import url("panels/unitcontrol.css"); @import url("panels/unitinfo.css"); diff --git a/client/public/stylesheets/panels/connectionstatus.css b/client/public/stylesheets/panels/connectionstatus.css index 715850a9..2b7dcec4 100644 --- a/client/public/stylesheets/panels/connectionstatus.css +++ b/client/public/stylesheets/panels/connectionstatus.css @@ -11,7 +11,7 @@ } #connection-status-panel[data-is-connected] dt::before { - content: "Connected FPS: " attr(data-framerate) " Load: " attr(data-load); + content: "Connected"; } #connection-status-panel[data-is-connected] dd::after { diff --git a/client/public/stylesheets/panels/serverstatus.css b/client/public/stylesheets/panels/serverstatus.css new file mode 100644 index 00000000..c001de99 --- /dev/null +++ b/client/public/stylesheets/panels/serverstatus.css @@ -0,0 +1,43 @@ +#server-status-panel { + display: flex; + flex-direction: row; + justify-content: space-between; + column-gap: 10px; +} + +#server-status-panel .ol-data-grid { + width: 100%; +} + +#server-status-panel .ol-data-grid:first-of-type { + border-right: 1px solid gray; + padding-right: 10px; +} + +#server-status-panel dd { + font-weight: bold; +} + +.fps-low { + color: red; +} + +.fps-medium { + color: orange; +} + +.fps-high { + color: lightgreen; +} + +.load-low { + color: lightgreen; +} + +.load-medium { + color: orange; +} + +.load-high { + color: red; +} \ No newline at end of file diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 7e47dff6..54e2f760 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -1,6 +1,5 @@ import { LatLng, LatLngBounds } from "leaflet"; -export const HIDE_ALL = "Hide all"; export const GAME_MASTER = "Game master"; export const BLUE_COMMANDER = "Blue commander"; export const RED_COMMANDER = "Red commander"; diff --git a/client/src/index.ts b/client/src/index.ts index 97a8718e..46ff79f4 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -17,6 +17,7 @@ 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"; var map: Map; @@ -28,6 +29,7 @@ var atc: ATC; var unitInfoPanel: UnitInfoPanel; var connectionStatusPanel: ConnectionStatusPanel; +var serverStatusPanel: ServerStatusPanel; var unitControlPanel: UnitControlPanel; var mouseInfoPanel: MouseInfoPanel; var logPanel: LogPanel; @@ -53,9 +55,10 @@ function setup() { unitInfoPanel = new UnitInfoPanel("unit-info-panel"); unitControlPanel = new UnitControlPanel("unit-control-panel"); connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel"); + serverStatusPanel = new ServerStatusPanel("server-status-panel"); mouseInfoPanel = new MouseInfoPanel("mouse-info-panel"); hotgroupPanel = new HotgroupPanel("hotgroup-panel"); - //logPanel = new LogPanel("log-panel"); + /* Popups */ infoPopup = new Popup("info-popup"); @@ -243,6 +246,10 @@ export function getConnectionStatusPanel() { return connectionStatusPanel; } +export function getServerStatusPanel() { + return serverStatusPanel; +} + export function getHotgroupPanel() { return hotgroupPanel; } diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 88758a00..8bad025d 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -46,6 +46,7 @@ export class Map extends L.Map { #miniMapLayerGroup: L.LayerGroup; #temporaryMarkers: TemporaryUnitMarker[] = []; #selecting: boolean = false; + #isZooming: boolean = false; #destinationGroupRotation: number = 0; #computeDestinationRotation: boolean = false; @@ -67,7 +68,7 @@ export class Map extends L.Map { constructor(ID: string) { /* Init the leaflet map */ //@ts-ignore Needed because the boxSelect option is non-standard - super(ID, { preferCanvas: true, doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); + super(ID, { zoomSnap: 0, zoomDelta: 0.25, preferCanvas: true, doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); this.setView([37.23, -115.8], 10); this.#ID = ID; @@ -93,7 +94,8 @@ export class Map extends L.Map { /* Register event handles */ this.on("click", (e: any) => this.#onClick(e)); this.on("dblclick", (e: any) => this.#onDoubleClick(e)); - this.on("zoomstart", (e: any) => this.#onZoom(e)); + this.on("zoomstart", (e: any) => this.#onZoomStart(e)); + this.on("zoomend", (e: any) => this.#onZoomEnd(e)); this.on("drag", (e: any) => this.centerOnUnit(null)); this.on("contextmenu", (e: any) => this.#onContextMenu(e)); this.on('selectionstart', (e: any) => this.#onSelectionStart(e)); @@ -270,6 +272,10 @@ export class Map extends L.Map { this.#coalitionAreaContextMenu.hide(); } + isZooming() { + return this.#isZooming; + } + /* Mouse coordinates */ getMousePosition() { return this.#lastMousePosition; @@ -544,11 +550,17 @@ export class Map extends L.Map { this.#updateDestinationCursors(); } - #onZoom(e: any) { + #onZoomStart(e: any) { if (this.#centerUnit != null) this.#panToUnit(this.#centerUnit); + this.#isZooming = true; } + #onZoomEnd(e: any) { + this.#isZooming = false; + } + + #panToUnit(unit: Unit) { var unitPosition = new L.LatLng(unit.getPosition().lat, unit.getPosition().lng); this.setView(unitPosition, this.getZoom(), { animate: false }); diff --git a/client/src/panels/connectionstatuspanel.ts b/client/src/panels/connectionstatuspanel.ts index 686595c2..54a3f7f6 100644 --- a/client/src/panels/connectionstatuspanel.ts +++ b/client/src/panels/connectionstatuspanel.ts @@ -8,12 +8,4 @@ export class ConnectionStatusPanel extends Panel { update(connected: boolean) { this.getElement().toggleAttribute( "data-is-connected", connected ); } - - setMetrics(frameRate: number, load: number) { - const dt = this.getElement().querySelector("dt"); - if (dt) { - dt.dataset["framerate"] = String(frameRate); - dt.dataset["load"] = String(load); - } - } } \ No newline at end of file diff --git a/client/src/panels/serverstatuspanel.ts b/client/src/panels/serverstatuspanel.ts new file mode 100644 index 00000000..33cb1b7a --- /dev/null +++ b/client/src/panels/serverstatuspanel.ts @@ -0,0 +1,26 @@ +import { Panel } from "./panel"; + +export class ServerStatusPanel extends Panel { + constructor(ID: string) { + super(ID); + } + + update(frameRate: number, load: number) { + const frameRateEl = this.getElement().querySelector("#server-frame-rate"); + if (frameRateEl) { + frameRateEl.textContent = `${frameRate}`; + frameRateEl.classList.toggle("fps-high", frameRate >= 60) + frameRateEl.classList.toggle("fps-medium", frameRate >= 30 && frameRate < 60) + frameRateEl.classList.toggle("fps-low", frameRate <= 30) + } + + const loadEl = this.getElement().querySelector("#server-load"); + if (loadEl) { + loadEl.textContent = `${load}`; + loadEl.classList.toggle("load-high", load >= 1000) + loadEl.classList.toggle("load-medium", load >= 100 && load < 1000) + loadEl.classList.toggle("load-low", load <= 100) + } + + } +} \ No newline at end of file diff --git a/client/src/server/server.ts b/client/src/server/server.ts index b8085471..24a160b1 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,5 +1,5 @@ import { LatLng } from 'leaflet'; -import { getConnectionStatusPanel, getInfoPopup, getMissionHandler, getUnitDataTable, getUnitsManager, setLoginStatus } from '..'; +import { getConnectionStatusPanel, getInfoPopup, getMissionData, getServerStatusPanel, getUnitDataTable, getUnitsManager, setLoginStatus } from '..'; import { GeneralSettings, Radio, TACAN } from '../@types/unit'; import { ROEs, emissionsCountermeasures, reactionsToThreat } from '../constants/constants'; @@ -59,8 +59,8 @@ export function GET(callback: CallableFunction, uri: string, options?: { time?: const result = JSON.parse(xmlHttp.responseText); lastUpdateTimes[uri] = callback(result); - if ("frameRate" in result && "load" in result) - getConnectionStatusPanel().setMetrics(result.frameRate, result.load); + if (result.frameRate !== undefined && result.load !== undefined) + getServerStatusPanel().update(result.frameRate, result.load); } } else if (xmlHttp.status == 401) { /* Bad credentials */ diff --git a/client/src/units/dataextractor.ts b/client/src/units/dataextractor.ts index 9390be62..278d4a72 100644 --- a/client/src/units/dataextractor.ts +++ b/client/src/units/dataextractor.ts @@ -64,9 +64,13 @@ export class DataExtractor { extractString(length?: number) { if (length === undefined) length = this.extractUInt16() - const value = this.#decoder.decode(this.#buffer.slice(this.#seekPosition, this.#seekPosition + length)); + var stringBuffer = this.#buffer.slice(this.#seekPosition, this.#seekPosition + length); + var view = new Int8Array(stringBuffer); + var stringLength = length; + view.forEach((value: number, idx: number) => { if (value === 0) stringLength = idx; }); + const value = this.#decoder.decode(stringBuffer); this.#seekPosition += length; - return value; + return value.substring(0, stringLength).trim(); } extractChar() { diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index af2dd847..0e289eeb 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -6,7 +6,7 @@ import { CustomMarker } from '../map/custommarker'; import { SVGInjector } from '@tanem/svg-injector'; import { UnitDatabase } from './unitdatabase'; import { TargetMarker } from '../map/targetmarker'; -import { BLUE_COMMANDER, BOMBING, CARPET_BOMBING, DLINK, DataIndexes, FIRE_AT_AREA, GAME_MASTER, HIDE_ALL, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, RED_COMMANDER, ROEs, RWR, VISUAL, emissionsCountermeasures, reactionsToThreat, states } from '../constants/constants'; +import { BLUE_COMMANDER, BOMBING, CARPET_BOMBING, DLINK, DataIndexes, FIRE_AT_AREA, GAME_MASTER, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, RED_COMMANDER, ROEs, RWR, VISUAL, emissionsCountermeasures, reactionsToThreat, states } from '../constants/constants'; import { Ammo, Contact, GeneralSettings, Offset, Radio, TACAN, UnitIconOptions } from '../@types/unit'; import { DataExtractor } from './dataextractor'; import { groundUnitDatabase } from './groundunitdatabase'; @@ -157,6 +157,7 @@ export class Unit extends CustomMarker { this.on('contextmenu', (e) => this.#onContextMenu(e)); this.on('mouseover', () => { this.setHighlighted(true); }) this.on('mouseout', () => { this.setHighlighted(false); }) + getMap().on("zoomend", () => {this.#onZoom();}) /* Deselect units if they are hidden */ document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { @@ -165,9 +166,7 @@ export class Unit extends CustomMarker { document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => { window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300); - }); - - getMap().on("zoomend", () => {this.#onZoom();}) + }); } getCategory() { @@ -379,8 +378,6 @@ export class Unit extends CustomMarker { } belongsToCommandedCoalition() { - if (getUnitsManager().getCommandMode() === HIDE_ALL) - return false; if (getUnitsManager().getCommandedCoalition() !== this.#coalition) return false; return true; @@ -497,7 +494,6 @@ export class Unit extends CustomMarker { (this.#controlled == false && hiddenUnits.includes("dcs")) || (hiddenUnits.includes(this.getMarkerCategory())) || (hiddenUnits.includes(this.#coalition)) || - (getUnitsManager().getCommandMode() === HIDE_ALL) || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0) || (!this.#isLeader && this.getCategory() == "GroundUnit" && getMap().getZoom() < 13)) && !(this.getSelected()); @@ -510,7 +506,10 @@ export class Unit extends CustomMarker { /* Add the marker if not present */ if (!getMap().hasLayer(this) && !this.getHidden()) { - this.addTo(getMap()); + if (getMap().isZooming()) + this.once("zoomend", () => {this.addTo(getMap())}) + else + this.addTo(getMap()); } /* Hide the marker if necessary*/ diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index dbade62b..c19e4afd 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -5,7 +5,7 @@ import { cloneUnit, deleteUnit, spawnAircrafts, spawnGroundUnits, spawnHelicopte import { bearingAndDistanceToLatLng, deg2rad, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils"; import { CoalitionArea } from "../map/coalitionarea"; import { groundUnitDatabase } from "./groundunitdatabase"; -import { BLUE_COMMANDER, DataIndexes, GAME_MASTER, HIDE_ALL, IADSDensities, IDLE, MOVE_UNIT, RED_COMMANDER } from "../constants/constants"; +import { BLUE_COMMANDER, DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT, RED_COMMANDER } from "../constants/constants"; import { DataExtractor } from "./dataextractor"; import { Contact } from "../@types/unit"; import { citiesDatabase } from "./citiesdatabase"; @@ -19,7 +19,7 @@ export class UnitsManager { #selectionEventDisabled: boolean = false; #pasteDisabled: boolean = false; #hiddenTypes: string[] = []; - #commandMode: string = HIDE_ALL; + #commandMode: string = GAME_MASTER; #requestDetectionUpdate: boolean = false; constructor() { @@ -96,7 +96,7 @@ export class UnitsManager { this.#units[ID]?.setData(dataExtractor); } - if (this.#requestDetectionUpdate) { + if (this.#requestDetectionUpdate && this.getCommandMode() != GAME_MASTER) { for (let ID in this.#units) { var unit = this.#units[ID]; if (!unit.belongsToCommandedCoalition()) diff --git a/client/views/index.ejs b/client/views/index.ejs index 2aabcabb..c7ab66e4 100644 --- a/client/views/index.ejs +++ b/client/views/index.ejs @@ -24,6 +24,7 @@ <%- include('panels/unitinfo.ejs') %> <%- include('panels/mouseinfo.ejs') %> <%- include('panels/connectionstatus.ejs') %> + <%- include('panels/serverstatus.ejs') %> <%- include('panels/hotgroup.ejs') %>