diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css index 0bb40f8b..2fea9002 100644 --- a/client/public/stylesheets/layout.css +++ b/client/public/stylesheets/layout.css @@ -41,16 +41,15 @@ body { margin: 50px; } -#settings-panel { +#primary-toolbar { position: absolute; left: 10px; - height: fit-content; - width: fit-content; top: 10px; z-index: 1000; - display: flex; - align-items: center; - column-gap: 10px; +} + +.content #primary-toolbar { + position: static; } #unit-control-panel { diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 569a17af..263f2cba 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -1,6 +1,7 @@ @import url("layout.css"); @import url("airbases.css"); @import url("contextmenu.css"); +@import url("units.css"); /* Variables definitions */ :root { diff --git a/client/src/@types/server.d.ts b/client/src/@types/server.d.ts index 4eb5c8ba..2cdba425 100644 --- a/client/src/@types/server.d.ts +++ b/client/src/@types/server.d.ts @@ -1,6 +1,15 @@ -interface ServerData { - units: {[key: string]: UnitData}, - bullseye: any, //TODO - airbases: any, //TODO - logs: any //TODO +interface UnitsData { + units: {[key: string]: UnitData}, +} + +interface AirbasesData { + airbases: {[key: string]: any}, +} + +interface BullseyesData { + bullseyes: {[key: string]: any}, +} + +interface LogData { + logs: {[key: string]: string}, } \ No newline at end of file diff --git a/client/src/@types/unit.d.ts b/client/src/@types/unit.d.ts index 0f7e9be7..43273b8d 100644 --- a/client/src/@types/unit.d.ts +++ b/client/src/@types/unit.d.ts @@ -20,7 +20,6 @@ interface FormationData { isLeader: boolean; isWingman: boolean; leaderID: number; - wingmen: Unit[]; wingmenIDs: number[]; } diff --git a/client/src/index.ts b/client/src/index.ts index 01a39aec..d0be2d05 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -1,22 +1,22 @@ import { Map } from "./map/map" -import { getDataFromDCS } from "./server/server" import { UnitsManager } from "./units/unitsmanager"; import { UnitInfoPanel } from "./panels/unitinfopanel"; import { ContextMenu } from "./controls/contextmenu"; import { ConnectionStatusPanel } from "./panels/connectionstatuspanel"; -import { MissionData } from "./missiondata/missiondata"; +import { MissionHandler } from "./missionhandler/missionhandler"; import { UnitControlPanel } from "./panels/unitcontrolpanel"; import { MouseInfoPanel } from "./panels/mouseinfopanel"; import { AIC } from "./aic/aic"; import { ATC } from "./atc/ATC"; import { FeatureSwitches } from "./FeatureSwitches"; import { LogPanel } from "./panels/logpanel"; +import { getAirbases, getBulllseye, getUnits } from "./server/server"; var map: Map; var contextMenu: ContextMenu; var unitsManager: UnitsManager; -var missionData: MissionData; +var missionHandler: MissionHandler; var aic: AIC; var atc: ATC; @@ -29,7 +29,6 @@ var logPanel: LogPanel; var connected: boolean = false; var activeCoalition: string = "blue"; -var refreshData: boolean = true; var featureSwitches; @@ -40,7 +39,7 @@ function setup() { /* Initialize */ map = new Map('map-container'); unitsManager = new UnitsManager(); - missionData = new MissionData(); + missionHandler = new MissionHandler(); contextMenu = new ContextMenu("contextmenu"); @@ -50,7 +49,7 @@ function setup() { mouseInfoPanel = new MouseInfoPanel("mouse-info-panel"); //logPanel = new LogPanel("log-panel"); - missionData = new MissionData(); + missionHandler = new MissionHandler(); /* AIC */ let aicFeatureSwitch = featureSwitches.getSwitch( "aic" ); @@ -76,31 +75,28 @@ function setup() { } /* On the first connection, force request of full data */ - requestUpdate(); - refreshData = false; + getAirbases((data: AirbasesData) => getMissionData()?.update(data)); + getBulllseye((data: BullseyesData) => getMissionData()?.update(data)); + getUnits((data: UnitsData) => getUnitsManager()?.update(data), true /* Does a full refresh */); + + /* Start periodically requesting updates */ + requestUpdate(true /* Start looping */); } -function requestUpdate() { - getDataFromDCS(refreshData, update); - +function requestUpdate(loop: boolean) { /* Main update rate = 250ms is minimum time, equal to server update time. */ - setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); + getUnits((data: UnitsData) => getUnitsManager()?.update(data)) + setTimeout(() => requestUpdate(loop), getConnected() ? 250 : 1000); getConnectionStatusPanel()?.update(getConnected()); } -export function update(data: ServerData) { - getUnitsManager()?.update(data); - getMissionData()?.update(data); - getLogPanel()?.update(data); -} - export function getMap() { return map; } export function getMissionData() { - return missionData; + return missionHandler; } export function getContextMenu() { diff --git a/client/src/missiondata/airbase.ts b/client/src/missionhandler/airbase.ts similarity index 100% rename from client/src/missiondata/airbase.ts rename to client/src/missionhandler/airbase.ts diff --git a/client/src/missiondata/missiondata.ts b/client/src/missionhandler/missionhandler.ts similarity index 90% rename from client/src/missiondata/missiondata.ts rename to client/src/missionhandler/missionhandler.ts index 1320c71c..67f7e9ae 100644 --- a/client/src/missiondata/missiondata.ts +++ b/client/src/missionhandler/missionhandler.ts @@ -9,7 +9,7 @@ var bullseyeIcons = [ new Icon({ iconUrl: 'images/bullseye2.png', iconAnchor: [30, 30]}) ] -export class MissionData +export class MissionHandler { #bullseyes : any; //TODO declare interface #bullseyeMarkers: any; @@ -27,13 +27,17 @@ export class MissionData this.#airbasesMarkers = {}; } - update(data: ServerData) + update(data: BullseyesData | AirbasesData) { - this.#bullseyes = data.bullseye; - this.#airbases = data.airbases; - if (this.#bullseyes != null && this.#airbases != null) + if ("bullseyes" in data) { - this.#drawBullseye(); + this.#bullseyes = data.bullseyes; + this.#drawBullseyes(); + } + + if ("airbases" in data) + { + this.#airbases = data.airbases; this.#drawAirbases(); } } @@ -43,7 +47,7 @@ export class MissionData return this.#bullseyes; } - #drawBullseye() + #drawBullseyes() { for (let idx in this.#bullseyes) { diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 7c6675f9..5659cb23 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,267 +1,232 @@ import * as L from 'leaflet' import { setConnected } from '..'; +const DEMO = true; + /* Edit here to change server address */ -const RESTaddress = "http://localhost:30000/olympus"; +const REST_ADDRESS = "http://localhost:30000/olympus"; const UNITS_URI = "units"; -const FULL_UPDATE_URI = "full"; -const PARTIAL_UPDATE_URI = "partial"; +const REFRESH_URI = "refresh"; +const UPDATE_URI = "update"; const LOGS_URI = "logs"; const AIRBASES_URI = "airbases"; const BULLSEYE_URI = "bullseye"; -export function getDataFromDCS(refresh: boolean, callback: CallableFunction) { - /* Request the updated unit data from the server */ +export function GET(callback: CallableFunction, uri: string){ var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("GET", RESTaddress, true); - + xmlHttp.open("GET", `${REST_ADDRESS}/${uri}`, true); xmlHttp.onload = function (e) { var data = JSON.parse(xmlHttp.responseText); callback(data); - setConnected(true); }; - xmlHttp.onerror = function () { console.error("An error occurred during the XMLHttpRequest"); - setConnected(false); }; + xmlHttp.send(null); +} - var request = { "refresh": refresh } - xmlHttp.send(JSON.stringify(request)); +export function POST(request: object, callback: CallableFunction){ + var xhr = new XMLHttpRequest(); + xhr.open("PUT", REST_ADDRESS); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.onreadystatechange = () => { callback(); }; + xhr.send(JSON.stringify(request)); +} + +export function getAirbases(callback: CallableFunction) { + GET(callback, AIRBASES_URI); +} + +export function getBulllseye(callback: CallableFunction) { + GET(callback, BULLSEYE_URI); +} + +export function getLogs(callback: CallableFunction) { + GET(callback, LOGS_URI); +} + +export function getUnits(callback: CallableFunction, refresh: boolean = false) { + if (!DEMO) + GET(callback, `${UNITS_URI}/${refresh? REFRESH_URI: UPDATE_URI}}`); + else + callback(DEMO_UNITS_DATA); } export function addDestination(ID: number, path: any) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { }; - var command = { "ID": ID, "path": path } var data = { "setPath": command } - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function spawnSmoke(color: string, latlng: L.LatLng) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Added " + color + " smoke at " + ConvertDDToDMS(latlng.lat, false) + " " + ConvertDDToDMS(latlng.lng, true)); - } - }; - var command = { "color": color, "location": latlng }; var data = { "smoke": command } - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function spawnGroundUnit(type: string, latlng: L.LatLng, coalition: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Added " + coalition + " " + type + " at " + ConvertDDToDMS(latlng.lat, false) + " " + ConvertDDToDMS(latlng.lng, true)); - } - }; - var command = { "type": type, "location": latlng, "coalition": coalition }; var data = { "spawnGround": command } - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function spawnAircraft(type: string, latlng: L.LatLng, coalition: string, payloadName: string | null = null, airbaseName: string | null = null) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Added " + coalition + " " + type + " at " + ConvertDDToDMS(latlng.lat, false) + " " + ConvertDDToDMS(latlng.lng, true)); - } - }; - var command = { "type": type, "location": latlng, "coalition": coalition, "payloadName": payloadName != null? payloadName: "", "airbaseName": airbaseName != null? airbaseName: ""}; var data = { "spawnAir": command } - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function attackUnit(ID: number, targetID: number) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " attack " + getUnitsManager().getUnitByID(targetID).unitName); - } - }; - var command = { "ID": ID, "targetID": targetID }; var data = { "attackUnit": command } - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function cloneUnit(ID: number, latlng: L.LatLng) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned"); - } - }; - var command = { "ID": ID, "location": latlng }; var data = { "cloneUnit": command } - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function deleteUnit(ID: number) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned"); - } - }; - var command = { "ID": ID}; var data = { "deleteUnit": command } - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function landAt(ID: number, latlng: L.LatLng) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned"); - } - }; - var command = { "ID": ID, "location": latlng }; var data = { "landAt": command } - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function changeSpeed(ID: number, speedChange: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - var command = {"ID": ID, "change": speedChange} var data = {"changeSpeed": command} - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function setSpeed(ID: number, speed: number) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - var command = {"ID": ID, "speed": speed} var data = {"setSpeed": command} - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function changeAltitude(ID: number, altitudeChange: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " altitude change request: " + altitudeChange); - } - }; - var command = {"ID": ID, "change": altitudeChange} var data = {"changeAltitude": command} - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function setAltitude(ID: number, altitude: number) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - var command = {"ID": ID, "altitude": altitude} var data = {"setAltitude": command} - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function createFormation(ID: number, isLeader: boolean, wingmenIDs: number[]) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " created formation with: " + wingmenIDs); - } - }; - var command = {"ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader} var data = {"setLeader": command} - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function setROE(ID: number, ROE: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - var command = {"ID": ID, "ROE": ROE} var data = {"setROE": command} - - xhr.send(JSON.stringify(data)); + POST(data, () => { }); } export function setReactionToThreat(ID: number, reactionToThreat: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - var command = {"ID": ID, "reactionToThreat": reactionToThreat} var data = {"setReactionToThreat": command} - - xhr.send(JSON.stringify(data)); + 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", + } + } + }, + bullseyes: [], + airbases: [] } \ No newline at end of file diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index a351d08b..40206e2a 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -49,26 +49,33 @@ export class Unit extends Marker { this.on('contextmenu', (e) => this.#onContextMenu(e)); var icon = new DivIcon({ - html: ` - - - - 3 - - - ${aircraftDatabase.getShortLabelByName(this.getData().name)} - - - - - - - - ${this.getData().unitName} - - - - `, + html: ` +
+
+
+
+
+
+
4
+
+
+
${aircraftDatabase.getShortLabelByName(this.getData().name)}
+
+
+
+
+
+
+
+
+
+
+
${this.getData().unitName}
+
+
+
+
+ `, className: 'ol-unit-marker', iconAnchor: [60, 60] }); @@ -287,7 +294,7 @@ export class Unit extends Marker { this.setLatLng(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); var element = this.getElement(); if (element != null) { - element.querySelector(".unit-vvi")?.setAttribute("style", `transform:rotate( calc( var( --unit-marker-air-vvi-rotation-offset ) + ${rad2deg(this.getFlightData().heading)}deg ) ); height:calc( ( var( --unit-marker-air-height ) / 2 ) + ${this.getFlightData().speed / 5}px );`); + element.querySelector(".unit-vvi")?.setAttribute("style", `style="height: ${this.getFlightData().speed / 5}px; transform:rotate(${rad2deg(this.getFlightData().heading)}deg);`); element.querySelector(".unit")?.setAttribute("data-fuel-level", "20"); element.querySelector(".unit")?.setAttribute("data-has-fox-1", "true"); diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 83ebe814..32c38aa8 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -36,12 +36,11 @@ export class UnitsManager { } } - removeUnit(ID: number) { } - update(data: ServerData) { + 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)) { diff --git a/client/views/index.ejs b/client/views/index.ejs index a6ad860b..c4b7d71a 100644 --- a/client/views/index.ejs +++ b/client/views/index.ejs @@ -26,9 +26,11 @@ <%- include('atc.ejs') %> <%- include('contextmenu.ejs') %> - <%- include('unitcontrol.ejs') %> + <%- include('unitcontrolpanel.ejs') %> + <%- include('unitinfopanel.ejs') %> + <%- include('mouseinfopanel.ejs') %> <%- include('navbar.ejs') %> - <%- include('mouseinfo.ejs') %> + <%- include('connectionstatuspanel.ejs') %> <% /* %> <%- include('log.ejs') %> diff --git a/client/views/navbar.ejs b/client/views/navbar.ejs index 06b70fca..f04919b6 100644 --- a/client/views/navbar.ejs +++ b/client/views/navbar.ejs @@ -1,7 +1,8 @@