Continue migration to node express typescript

This commit is contained in:
Pax1601
2023-01-15 17:48:55 +01:00
parent a75a491fe7
commit baf288c6a0
58 changed files with 4232 additions and 105 deletions

View File

@@ -0,0 +1,51 @@
export class SelectionScroll
{
#container : HTMLElement | null;
#display: string;
constructor(id: string, )
{
this.#container = document.getElementById(id);
this.#display = '';
if (this.#container != null)
{
this.#display = this.#container.style.display;
this.hide();
}
}
show(x: number, y: number, options: any, callback: CallableFunction)
{
/* Hide to remove buttons, if present */
this.hide();
if (this.#container != null && options.length > 1)
{
this.#container.style.display = this.#display;
this.#container.style.left = x - 110 + "px";
this.#container.style.top = y - 110 + "px";
for (let optionID in options)
{
var node = document.createElement("div");
node.classList.add("olympus-selection-scroll-element");
node.appendChild(document.createTextNode(options[optionID]));
this.#container.appendChild(node);
node.addEventListener('click', () => callback(options[optionID]))
}
}
}
hide()
{
if (this.#container != null)
{
this.#container.style.display = "none";
var buttons = this.#container.querySelectorAll(".olympus-selection-scroll-element");
for (let child of buttons)
{
this.#container.removeChild(child);
}
}
}
}

View File

@@ -0,0 +1,92 @@
import { setActiveCoalition } from "..";
import { deg2rad } from "../other/utils";
export class SelectionWheel
{
#container: HTMLElement | null;
#display: string;
constructor(id: string)
{
this.#container = document.getElementById(id);
this.#display = '';
if (this.#container != null)
{
this.#container.querySelector("#coalition-switch")?.addEventListener('change', (e) => this.#onSwitch(e))
this.#display = this.#container.style.display;
this.hide();
}
}
show(x: number, y: number, options: any, showCoalition: boolean)
{
/* Hide to remove buttons, if present */
this.hide();
if (this.#container != null)
{
this.#container.style.display = this.#display;
this.#container.style.left = x - 110 + "px";
this.#container.style.top = y - 110 + "px";
var angularSize = 360 / options.length;
var r = 80;
/* Create the buttons */
for (let id in options)
{
var button = document.createElement("div");
button.classList.add("selection-wheel-button");
button.style.left = x - 25 + "px";
button.style.top = y - 25 + "px";
button.addEventListener('click', (e) => options[id].callback(e));
this.#container.appendChild(button);
var angle = parseInt(id) * angularSize;
button.style.opacity = "1";
button.style.left = x + r * Math.sin(deg2rad(angle)) - 25 + "px";
button.style.top = y - r * Math.cos(deg2rad(angle)) - 25 + "px";
var image = document.createElement("img");
image.classList.add("selection-wheel-image");
image.src = `images/buttons/${options[id].src}`
image.title = options[id].tooltip;
if ('tint' in options[id])
{
button.style.setProperty('background-color', options[id].tint);
image.style.opacity = "0";
}
button.appendChild(image);
}
}
}
hide()
{
if (this.#container != null)
{
this.#container.style.display = "none";
var buttons = this.#container.querySelectorAll(".selection-wheel-button");
for (let child of buttons)
{
this.#container.removeChild(child);
}
}
}
#onSwitch(e: any)
{
if (this.#container != null)
{
if (e.currentTarget.checked)
{
document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--red-coalition-color"));
setActiveCoalition("red");
}
else
{
document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--blue-coalition-color"));
setActiveCoalition("blue");
}
}
}
}

122
client/src/dcs/dcs.ts Normal file
View File

@@ -0,0 +1,122 @@
import * as L from 'leaflet'
import { ConvertDDToDMS } from '../other/utils';
/* Edit here to change server address */
var RESTaddress = "http://localhost:30000/restdemo";
export function getDataFromDCS(callback: CallableFunction)
{
/* Request the updated unit data from the server */
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", RESTaddress, true);
xmlHttp.onload = function(e)
{
var data = JSON.parse(xmlHttp.responseText);
callback(data);
};
xmlHttp.onerror = function () {
console.error("An error occurred during the XMLHttpRequest");
};
xmlHttp.send( null );
}
export function addDestination(ID: number, path: any)
{
// TODO move in dedicated file
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));
}
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));
}
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));
}
export function spawnAircraft(type: string, latlng: L.LatLng, coalition: string, payloadName = "", airbaseName = "")
{
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, "airbaseName": airbaseName};
var data = {"spawnAir": command}
xhr.send(JSON.stringify(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 " + unitsManager.getUnitByID(ID).unitName + " attack " + unitsManager.getUnitByID(targetID).unitName );
// }
//};
//
//var command = {"ID": ID, "targetID": targetID};
//var data = {"attackUnit": command}
//
//xhr.send(JSON.stringify(data));
}
export function cloneUnit(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 " + unitsManager.getUnitByID(ID).unitName + " cloned");
// }
//};
//
//var command = {"ID": ID};
//var data = {"cloneUnit": command}
//
//xhr.send(JSON.stringify(data));
}

View File

@@ -1,3 +1,68 @@
import { Olympus } from "./map/map"
import { Map } from "./map/map"
import { getDataFromDCS } from "./dcs/dcs"
import { SelectionWheel } from "./controls/selectionwheel";
import { UnitsManager } from "./units/unitsmanager";
import { UnitInfoPanel } from "./panels/unitinfopanel";
import { SelectionScroll } from "./controls/selectionscroll";
var map = new Olympus.Map('olympus-map-container');
var map: Map;
var selectionWheel: SelectionWheel;
var selectionScroll: SelectionScroll;
var unitsManager: UnitsManager;
var unitInfoPanel: UnitInfoPanel;
var activeCoalition: string
function setup()
{
/* Initialize */
map = new Map('olympus-map-container');
selectionWheel = new SelectionWheel("selection-wheel");
selectionScroll = new SelectionScroll("selection-scroll");
unitsManager = new UnitsManager();
unitInfoPanel = new UnitInfoPanel("olympus-unit-info-panel");
/* Main update rate = 250ms is minimum time, equal to server update time. */
setInterval(() => getDataFromDCS(update), 250);
}
function update(data: JSON)
{
unitsManager.update(data);
}
export function getMap()
{
return map;
}
export function getSelectionWheel()
{
return selectionWheel;
}
export function getSelectionScroll()
{
return selectionScroll;
}
export function getUnitsManager()
{
return unitsManager;
}
export function getUnitInfoPanel()
{
return unitInfoPanel;
}
export function setActiveCoalition(coalition: string)
{
activeCoalition = coalition;
}
export function getActiveCoalition()
{
return activeCoalition;
}
window.onload = setup;

View File

@@ -1,17 +1,189 @@
import * as L from 'leaflet'
import * as L from "leaflet"
import { getSelectionWheel, getSelectionScroll, getUnitsManager, getActiveCoalition } from "..";
import { spawnAircraft } from "../dcs/dcs";
import { payloadNames } from "../units/payloadNames";
import { unitTypes } from "../units/unitTypes";
export namespace Olympus
export class Map extends L.Map
{
export class Map extends L.Map
{
constructor(containerId: string)
{
super(containerId, {doubleClickZoom: false});
this.setView([37.23, -115.8], 12);
#state: string;
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
}).addTo(this);
constructor(containerId: string)
{
/* Init the leaflet map */
super(containerId, {doubleClickZoom: false});
this.setView([37.23, -115.8], 12);
L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", {
attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community"
}).addTo(this);
/* Init the state machine */
this.#state = "IDLE";
/* Register event handles */
this.on("contextmenu", (e) => this.#onContextMenu(e));
this.on("click", (e) => this.#onClick(e));
this.on("dblclick", (e) => this.#onDoubleClick(e));
}
/* State machine */
setState(state: string)
{
this.#state = state;
if (this.#state === "IDLE")
{
}
}
}
else if (this.#state === "MOVE_UNIT")
{
}
else if (this.#state === "ATTACK")
{
}
else if (this.#state === "FORMATION")
{
}
}
getState()
{
return this.#state;
}
/* Selection wheel */
showSelectionWheel(e: PointerEvent, options: any, showCoalition: boolean)
{
var x = e.x;
var y = e.y;
getSelectionWheel().show(x, y, options, showCoalition);
}
hideSelectionWheel()
{
getSelectionWheel().hide();
}
/* Selection scroll */
showSelectionScroll(e: PointerEvent, options: any, callback: CallableFunction)
{
var x = e.x;
var y = e.y;
getSelectionScroll().show(x, y, options, callback);
}
hideSelectionScroll()
{
getSelectionScroll().hide();
}
/* Event handlers */
#onContextMenu(e: any)
{
this.setState("IDLE");
getUnitsManager().deselectAllUnits();
this.hideSelectionWheel();
this.hideSelectionScroll();
}
#onClick(e: any)
{
this.hideSelectionWheel();
this.hideSelectionScroll();
if (this.#state === "IDLE")
{
}
else if (this.#state === "MOVE_UNIT")
{
if (!e.originalEvent.ctrlKey)
{
//unitsManager.clearDestinations();
}
//unitsManager.addDestination(e.latlng)
}
}
#onDoubleClick(e: any)
{
if (this.#state == "IDLE")
{
var options = [
{"tooltip": "Air unit", "src": "spawnAir.png", "callback": () => this.#aircraftSpawnMenu(e)},
{"tooltip": "Ground unit", "src": "spawnGround.png", "callback": () => this.#groundUnitSpawnMenu(e)},
{"tooltip": "Smoke", "src": "spawnSmoke.png", "callback": () => this.#smokeSpawnMenu(e)},
{"tooltip": "Explosion", "src": "spawnExplosion.png", "callback": () => this.#explosionSpawnMenu(e)}
]
this.showSelectionWheel(e.originalEvent, options, true);
}
}
/* Spawning menus */
#groundUnitSpawnMenu(e: any)
{
}
#smokeSpawnMenu(e: any)
{
}
#explosionSpawnMenu(e: any)
{
}
#aircraftSpawnMenu(e: any)
{
var options = [
{'coalition': true, 'tooltip': 'CAP', 'src': 'spawnCAP.png', 'callback': () => this.#selectAircraft(e, "CAP")},
{'coalition': true, 'tooltip': 'CAS', 'src': 'spawnCAS.png', 'callback': () => this.#selectAircraft(e, "CAS")},
{'coalition': true, 'tooltip': 'Tanker', 'src': 'spawnTanker.png', 'callback': () => this.#selectAircraft(e, "tanker")},
{'coalition': true, 'tooltip': 'AWACS', 'src': 'spawnAWACS.png', 'callback': () => this.#selectAircraft(e, "awacs")},
{'coalition': true, 'tooltip': 'Strike', 'src': 'spawnStrike.png', 'callback': () => this.#selectAircraft(e, "strike")},
{'coalition': true, 'tooltip': 'Drone', 'src': 'spawnDrone.png', 'callback': () => this.#selectAircraft(e, "drone")},
{'coalition': true, 'tooltip': 'Transport', 'src': 'spawnTransport.png','callback': () => this.#selectAircraft(e, "transport")},
]
this.showSelectionWheel(e.originalEvent, options, true);
}
/* Show unit selection for air units */
#selectAircraft(e: any, group: string)
{
this.hideSelectionWheel();
this.hideSelectionScroll();
var options = unitTypes.air[group];
options.sort();
this.showSelectionScroll(e.originalEvent, options, (unitType: string) => {
this.hideSelectionWheel();
this.hideSelectionScroll();
this.#unitSelectPayload(e, unitType);
});
}
/* Show weapon selection for air units */
#unitSelectPayload(e: any, unitType: string)
{
this.hideSelectionWheel();
this.hideSelectionScroll();
var options = [];
options = payloadNames[unitType]
if (options != undefined && options.length > 0)
{
options.sort();
this.showSelectionScroll(e.originalEvent, options, (payloadName: string) => {
this.hideSelectionWheel();
this.hideSelectionScroll();
spawnAircraft(unitType, e.latlng, getActiveCoalition(), payloadName, e.airbaseName);
});
}
else
{
spawnAircraft(unitType, e.latlng, getActiveCoalition());
}
}
}

65
client/src/other/utils.ts Normal file
View File

@@ -0,0 +1,65 @@
export function distance(lat1: number, lon1: number, lat2: number, lon2: number)
{
const R = 6371e3; // metres
const φ1 = deg2rad(lat1); // φ, λ in radians
const φ2 = deg2rad(lat2);
const Δφ = deg2rad(lat2-lat1);
const Δλ = deg2rad(lon2-lon1);
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const d = R * c; // in metres
return d;
}
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number)
{
const φ1 = deg2rad(lat1); // φ, λ in radians
const φ2 = deg2rad(lat2);
const λ1 = deg2rad(lon1); // φ, λ in radians
const λ2 = deg2rad(lon2);
const y = Math.sin(λ2-λ1) * Math.cos(φ2);
const x = Math.cos(φ1)*Math.sin(φ2) - Math.sin(φ1)*Math.cos(φ2)*Math.cos(λ2-λ1);
const θ = Math.atan2(y, x);
const brng = (rad2deg(θ) + 360) % 360; // in degrees
return brng;
}
const zeroPad = function(num: number, places: number)
{
var string = String(num);
while (string.length < places)
{
string += "0";
}
return string;
}
export function ConvertDDToDMS(D: number, lng: boolean)
{
var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N";
var deg = 0 | (D < 0 ? (D = -D) : D);
var min = 0 | (((D += 1e-9) % 1) * 60);
var sec = (0 | (((D * 60) % 1) * 6000)) / 100;
var dec = Math.round((sec - Math.floor(sec)) * 100);
var sec = Math.floor(sec);
if (lng)
return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
else
return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
}
export function deg2rad(deg: number)
{
var pi = Math.PI;
return deg * (pi/180);
}
export function rad2deg(rad: number)
{
var pi = Math.PI;
return rad / (pi/180);
}

View File

@@ -0,0 +1,43 @@
import { ConvertDDToDMS, rad2deg } from "../other/utils";
import { Unit } from "../units/unit";
export class UnitInfoPanel
{
#element: HTMLElement
#display: string;
constructor(ID: string)
{
this.#element = <HTMLElement>document.getElementById(ID);
this.#display = '';
if (this.#element != null)
{
this.#display = this.#element.style.display;
this.hide();
}
}
show()
{
this.#element.style.display = this.#display;
}
hide()
{
this.#element.style.display = "none";
}
update(unit: Unit)
{
if (this.#element != null)
{
this.#element.querySelector("#unit-name")!.innerHTML = unit.unitName;
this.#element.querySelector("#group-name")!.innerHTML = unit.groupName;
this.#element.querySelector("#heading")!.innerHTML = String(Math.floor(rad2deg(unit.heading)) + "°");
this.#element.querySelector("#altitude")!.innerHTML = String(Math.floor(unit.altitude / 0.3048) + "ft");
this.#element.querySelector("#groundspeed")!.innerHTML = String(Math.floor(unit.speed * 1.94384) + "kts");
//this.#element.querySelector("#altitude")!.innerHTML = String(Math.floor(unit.missionData.fuel * 100) + "%");
this.#element.querySelector("#position")!.innerHTML = ConvertDDToDMS(unit.latitude, false) + " " + ConvertDDToDMS(unit.longitude, true);
}
}
}

File diff suppressed because one or more lines are too long

512
client/src/units/unit.ts Normal file
View File

@@ -0,0 +1,512 @@
import { Marker, LatLng, Polyline } from 'leaflet';
import { ConvertDDToDMS } from '../other/utils';
import { getMap, getUnitsManager } from '..';
import { UnitMarker, MarkerOptions } from './unitmarker';
import { addDestination } from '../dcs/dcs';
//import { attackUnit } from 'DCS/DCSCommands.js'
export class Unit
{
ID : number;
leader : boolean;
wingman : boolean;
wingmen : Unit[];
formation : string;
name : string;
unitName : string;
groupName : string;
latitude : number;
longitude : number;
altitude : number;
heading : number;
coalitionID : number;
alive : boolean;
speed : number;
currentTask : string;
type : Object | null;
flags : Object | null;
activePath : any | null; // TODO: declare inteface
missionData : Object | null;
#selectable : boolean;
#selected : boolean;
#preventClick : boolean;
#pathMarkers : Marker[];
#pathPolyline : Polyline;
#targetsPolylines : Polyline[];
#marker : UnitMarker;
#timer : number;
static getConstructor(name: string)
{
if (name === "GroundUnit") return GroundUnit;
if (name === "Aircraft") return Aircraft;
if (name === "Helicopter") return Helicopter;
if (name === "Missile") return Missile;
if (name === "Bomb") return Bomb;
if (name === "NavyUnit") return NavyUnit;
}
constructor(ID: number, marker: UnitMarker)
{
this.ID = ID;
/* Names */
this.name = "";
this.unitName = "";
this.groupName = "";
/* Position and speed */
this.latitude = 0;
this.longitude = 0;
this.altitude = 0;
this.heading = 0;
this.speed = 0;
/* Tasking */
this.coalitionID = 0;
this.alive = true;
this.currentTask = "";
/* Formation */
this.leader = false;
this.wingman = false;
this.wingmen = [];
this.formation = "";
/* Structures */
this.type = null;
this.flags = null;
this.activePath = null;
this.missionData = null;
this.#selectable = true;
this.#timer = 0;
/* The marker is set by the inherited class */
this.#marker = marker;
this.#marker.on('click', (e) => this.#onClick(e));
//this.#marker.on('dblclick', (e) => this.#onDoubleClick(e));
this.#selected = false;
this.#preventClick = false;
this.#pathMarkers = [];
this.#pathPolyline = new Polyline([], {color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1});
this.#pathPolyline.addTo(getMap());
this.#targetsPolylines = [];
}
update(response: JSON)
{
for (let entry in response)
{
// @ts-ignore
this[entry] = response[entry];
}
this.#updateMarker();
if (this.getSelected())
this.#drawPath();
else
this.#clearPath();
/*
this.wingmen = [];
if (response["wingmenIDs"] != undefined)
{
for (let ID of response["wingmenIDs"])
{
this.wingmen.push(unitsManager.getUnitByID(ID));
}
}
this.formation = response["formation"];
this.missionData = missionData.getUnitData(this.ID)
this.setSelected(this.getSelected() && this.alive)
this.clearTargets();
this.missionData = missionData.getUnitData(this.ID);
if (this.missionData != undefined)
{
if (this.getSelected())
{
this.drawTargets();
}
}
*/
}
setSelected(selected: boolean)
{
// Only alive units can be selected. Some units are not selectable (weapons)
if ((this.alive || !selected) && this.#selectable && this.#selected != selected)
{
this.#selected = selected;
this.#marker.setSelected(selected);
getUnitsManager().onUnitSelection();
}
}
getSelected()
{
return this.#selected;
}
addDestination(latlng: L.LatLng)
{
var path: any = {};
if (this.activePath != undefined)
{
path = this.activePath;
path[(Object.keys(path).length + 1).toString()] = latlng;
}
else
{
path = {"1": latlng};
}
addDestination
}
clearDestinations()
{
this.activePath = undefined;
}
#onClick(e: any)
{
this.#timer = setTimeout(() => {
if (!this.#preventClick) {
if (getMap().getState() === 'IDLE' || getMap().getState() === 'MOVE_UNIT' || e.originalEvent.ctrlKey)
{
if (!e.originalEvent.ctrlKey)
{
getUnitsManager().deselectAllUnits();
}
this.setSelected(true);
}
}
this.#preventClick = false;
}, 200);
}
#updateMarker()
{
/* Add the marker if not present */
if (!getMap().hasLayer(this.#marker))
{
this.#marker.addTo(getMap());
}
this.#marker.setLatLng(new LatLng(this.latitude, this.longitude));
this.#marker.draw({
heading: this.heading,
speed: this.speed,
altitude: this.altitude,
alive: this.alive
});
}
#drawPath()
{
if (this.activePath != null)
{
var _points = [];
_points.push(new LatLng(this.latitude, this.longitude));
// Add markers if missing
while (this.#pathMarkers.length < Object.keys(this.activePath).length)
{
var marker = new Marker([0, 0]).addTo(getMap());
this.#pathMarkers.push(marker);
}
// Remove markers if too many
while (this.#pathMarkers.length > Object.keys(this.activePath).length)
{
getMap().removeLayer(this.#pathMarkers[this.#pathMarkers.length - 1]);
this.#pathMarkers.splice(this.#pathMarkers.length - 1, 1)
}
// Update the position of the existing markers (to avoid creating markers uselessly)
for (let WP in this.activePath)
{
var destination = this.activePath[WP];
this.#pathMarkers[parseInt(WP) - 1].setLatLng([destination.lat, destination.lng]);
_points.push(new LatLng(destination.lat, destination.lng));
this.#pathPolyline.setLatLngs(_points);
}
}
}
#clearPath()
{
for (let WP in this.#pathMarkers)
{
getMap().removeLayer(this.#pathMarkers[WP]);
}
this.#pathMarkers = [];
this.#pathPolyline.setLatLngs([]);
}
/*
#onDoubleClick(e)
{
clearTimeout(this.#timer);
this.#preventClick = true;
var options = [
{'tooltip': 'Attack', 'src': 'attack.png', 'callback': () => {map.removeSelectionWheel(); unitsManager.attackUnit(this.ID);}},
{'tooltip': 'Go to tanker', 'src': 'tanker.png', 'callback': () => {map.removeSelectionWheel(); showMessage("Function not implemented yet");}},
{'tooltip': 'RTB', 'src': 'rtb.png', 'callback': () => {map.removeSelectionWheel(); showMessage("Function not implemented yet");}}
]
if (!this.leader && !this.wingman)
{
options.push({'tooltip': 'Create formation', 'src': 'formation.png', 'callback': () => {map.removeSelectionWheel(); unitsManager.createFormation(this.ID);}});
}
map.showSelectionWheel(e, options, false);
}
drawPath()
{
var _points = [];
_points.push(new LatLng(this.latitude, this.longitude));
// Add markers if missing
while (this.#pathMarkers.length < Object.keys(this.activePath).length)
{
var marker = new Marker([0, 0]).addTo(map.getMap());
this.#pathMarkers.push(marker);
}
// Remove markers if too many
while (this.#pathMarkers.length > Object.keys(this.activePath).length)
{
map.getMap().removeLayer(this.#pathMarkers[this.#pathMarkers.length - 1]);
this.#pathMarkers.splice(this.#pathMarkers.length - 1, 1)
}
// Update the position of the existing markers (to avoid creating markers uselessly)
for (let WP in this.activePath)
{
var destination = this.activePath[WP];
this.#pathMarkers[parseInt(WP) - 1].setLatLng([destination.lat, destination.lng]);
_points.push(new LatLng(destination.lat, destination.lng));
this.#pathPolyline.setLatLngs(_points);
}
}
clearPath()
{
for (let WP in this.#pathMarkers)
{
map.getMap().removeLayer(this.#pathMarkers[WP]);
}
this.#pathMarkers = [];
this.#pathPolyline.setLatLngs([]);
}
drawTargets()
{
for (let typeIndex in this.missionData['targets'])
{
for (let index in this.missionData['targets'][typeIndex])
{
var targetData = this.missionData['targets'][typeIndex][index];
var target = unitsManager.getUnitByID(targetData.object["id_"])
if (target != undefined){
var startLatLng = new LatLng(this.latitude, this.longitude)
var endLatLng = new LatLng(target.latitude, target.longitude)
var color;
if (typeIndex === "radar")
{
color = "#FFFF00";
}
else if (typeIndex === "visual")
{
color = "#FF00FF";
}
else if (typeIndex === "rwr")
{
color = "#00FF00";
}
else
{
color = "#FFFFFF";
}
var targetPolyline = new Polyline([startLatLng, endLatLng], {color: color, weight: 3, opacity: 1, smoothFactor: 1});
targetPolyline.addTo(map.getMap());
this.#targetsPolylines.push(targetPolyline)
}
}
}
}
clearTargets()
{
for (let index in this.#targetsPolylines)
{
map.getMap().removeLayer(this.#targetsPolylines[index])
}
}
attackUnit(targetID)
{
// Call DCS attackUnit function
if (this.ID != targetID)
{
attackUnit(this.ID, targetID);
}
else
{
// TODO: show a message
}
}
changeSpeed(speedChange)
{
// TODO move in dedicated file
var xhr = new XMLHttpRequest();
xhr.open("PUT", RESTaddress);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log(this.unitName + " speed change request: " + speedChange);
}
};
var command = {"ID": this.ID, "change": speedChange}
var data = {"changeSpeed": command}
xhr.send(JSON.stringify(data));
}
changeAltitude(altitudeChange)
{
// TODO move in dedicated file
var xhr = new XMLHttpRequest();
xhr.open("PUT", RESTaddress);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log(this.unitName + " altitude change request: " + altitudeChange);
}
};
var command = {"ID": this.ID, "change": altitudeChange}
var data = {"changeAltitude": command}
xhr.send(JSON.stringify(data));
}
setformation(formation)
{
// TODO move in dedicated file
var xhr = new XMLHttpRequest();
xhr.open("PUT", RESTaddress);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log(this.unitName + " formation change: " + formation);
}
};
var command = {"ID": this.ID, "formation": formation}
var data = {"setFormation": command}
xhr.send(JSON.stringify(data));
}
setLeader(wingmenIDs)
{
// TODO move in dedicated file
var xhr = new XMLHttpRequest();
xhr.open("PUT", RESTaddress);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log(this.unitName + " created formation with: " + wingmenIDs);
}
};
var command = {"ID": this.ID, "wingmenIDs": wingmenIDs}
var data = {"setLeader": command}
xhr.send(JSON.stringify(data));
}
*/
}
export class AirUnit extends Unit
{
}
export class Aircraft extends AirUnit
{
constructor(ID: number, options: MarkerOptions)
{
var marker = new UnitMarker(options);
super(ID, marker);
}
}
export class Helicopter extends AirUnit
{
constructor(ID: number, options: MarkerOptions)
{
var marker = new UnitMarker(options);
super(ID, marker);
}
}
export class GroundUnit extends Unit
{
constructor(ID: number, options: MarkerOptions)
{
var marker = new UnitMarker(options);
super(ID, marker);
}
}
export class NavyUnit extends Unit
{
constructor(ID: number, options: MarkerOptions)
{
var marker = new UnitMarker(options);
super(ID, marker);
}
}
export class Weapon extends Unit
{
}
export class Missile extends Weapon
{
constructor(ID: number, options: MarkerOptions)
{
var marker = new UnitMarker(options);
super(ID, marker);
}
}
export class Bomb extends Weapon
{
constructor(ID: number, options: MarkerOptions)
{
var marker = new UnitMarker(options);
super(ID, marker);
}
}

View File

@@ -0,0 +1,276 @@
export var unitTypes: any = {};
/* NAVY */
unitTypes.navy = {};
unitTypes.navy.blue = [
"VINSON",
"PERRY",
"TICONDEROG"
]
unitTypes.navy.red = [
"ALBATROS",
"KUZNECOW",
"MOLNIYA",
"MOSCOW",
"NEUSTRASH",
"PIOTR",
"REZKY"
]
unitTypes.navy.civil = [
"ELNYA",
"Dry-cargo ship-2",
"Dry-cargo ship-1",
"ZWEZDNY"
]
unitTypes.navy.submarine = [
"KILO",
"SOM"
]
unitTypes.navy.speedboat = [
"speedboat"
]
/* VEHICLES (GROUND) */
unitTypes.vehicles = []
unitTypes.vehicles.Howitzers = [
"2B11 mortar",
"SAU Gvozdika",
"SAU Msta",
"SAU Akatsia",
"SAU 2-C9",
"M-109"
]
unitTypes.vehicles.IFV = [
"AAV7",
"BMD-1",
"BMP-1",
"BMP-2",
"BMP-3",
"Boman",
"BRDM-2",
"BTR-80",
"BTR_D",
"Bunker",
"Cobra",
"LAV-25",
"M1043 HMMWV Armament",
"M1045 HMMWV TOW",
"M1126 Stryker ICV",
"M-113",
"M1134 Stryker ATGM",
"M-2 Bradley",
"Marder",
"MCV-80",
"MTLB",
"Paratrooper RPG-16",
"Paratrooper AKS-74",
"Sandbox",
"Soldier AK",
"Infantry AK",
"Soldier M249",
"Soldier M4",
"Soldier M4 GRG",
"Soldier RPG",
"TPZ"
]
unitTypes.vehicles.MLRS = [
"Grad-URAL",
"Uragan_BM-27",
"Smerch",
"MLRS"
]
unitTypes.vehicles.SAM = [
"2S6 Tunguska",
"Kub 2P25 ln",
"5p73 s-125 ln",
"S-300PS 5P85C ln",
"S-300PS 5P85D ln",
"SA-11 Buk LN 9A310M1",
"Osa 9A33 ln",
"Tor 9A331",
"Strela-10M3",
"Strela-1 9P31",
"SA-11 Buk CC 9S470M1",
"SA-8 Osa LD 9T217",
"Patriot AMG",
"Patriot ECS",
"Gepard",
"Hawk pcp",
"SA-18 Igla manpad",
"SA-18 Igla comm",
"Igla manpad INS",
"SA-18 Igla-S manpad",
"SA-18 Igla-S comm",
"Vulcan",
"Hawk ln",
"M48 Chaparral",
"M6 Linebacker",
"Patriot ln",
"M1097 Avenger",
"Patriot EPP",
"Patriot cp",
"Roland ADS",
"S-300PS 54K6 cp",
"Stinger manpad GRG",
"Stinger manpad dsr",
"Stinger comm dsr",
"Stinger manpad",
"Stinger comm",
"ZSU-23-4 Shilka",
"ZU-23 Emplacement Closed",
"ZU-23 Emplacement",
"ZU-23 Closed Insurgent",
"Ural-375 ZU-23 Insurgent",
"ZU-23 Insurgent",
"Ural-375 ZU-23"
]
unitTypes.vehicles.Radar = [
"1L13 EWR",
"Kub 1S91 str",
"S-300PS 40B6M tr",
"S-300PS 40B6MD sr",
"55G6 EWR",
"S-300PS 64H6E sr",
"SA-11 Buk SR 9S18M1",
"Dog Ear radar",
"Hawk tr",
"Hawk sr",
"Patriot str",
"Hawk cwar",
"p-19 s-125 sr",
"Roland Radar",
"snr s-125 tr"
]
unitTypes.vehicles.Structures = [
"house1arm",
"house2arm",
"outpost_road",
"outpost",
"houseA_arm"
]
unitTypes.vehicles.Tanks = [
"Challenger2",
"Leclerc",
"Leopard1A3",
"Leopard-2",
"M-60",
"M1128 Stryker MGS",
"M-1 Abrams",
"T-55",
"T-72B",
"T-80UD",
"T-90"
]
unitTypes.vehicles.Unarmed = [
"Ural-4320 APA-5D",
"ATMZ-5",
"ATZ-10",
"GAZ-3307",
"GAZ-3308",
"GAZ-66",
"M978 HEMTT Tanker",
"HEMTT TFFT",
"IKARUS Bus",
"KAMAZ Truck",
"LAZ Bus",
"Hummer",
"M 818",
"MAZ-6303",
"Predator GCS",
"Predator TrojanSpirit",
"Suidae",
"Tigr_233036",
"Trolley bus",
"UAZ-469",
"Ural ATsP-6",
"Ural-375 PBU",
"Ural-375",
"Ural-4320-31",
"Ural-4320T",
"VAZ Car",
"ZiL-131 APA-80",
"SKP-11",
"ZIL-131 KUNG",
"ZIL-4331"
]
/* AIRPLANES */
unitTypes.air = {}
unitTypes.air.CAP = [
"F-4E",
"F/A-18C",
"MiG-29S",
"F-14A",
"Su-27",
"MiG-23MLD",
"Su-33",
"MiG-25RBT",
"Su-30",
"MiG-31",
"Mirage 2000-5",
"F-15C",
"F-5E",
"F-16C bl.52d",
]
unitTypes.air.CAS = [
"Tornado IDS",
"F-4E",
"F/A-18C",
"MiG-27K",
"A-10C",
"Su-25",
"Su-34",
"Su-17M4",
"F-15E",
]
unitTypes.air.strike = [
"Tu-22M3",
"B-52H",
"F-111F",
"Tu-95MS",
"Su-24M",
"Tu-160",
"F-117A",
"B-1B",
"Tu-142",
]
unitTypes.air.tank = [
"S-3B Tanker",
"KC-135",
"IL-78M",
]
unitTypes.air.awacs = [
"A-50",
"E-3A",
"E-2D",
]
unitTypes.air.drone = [
"MQ-1A Predator",
"MQ-9 Reaper",
]
unitTypes.air.transport = [
"C-130",
"An-26B",
"An-30M",
"C-17A",
"IL-76MD",
]

View File

@@ -0,0 +1,147 @@
import * as L from 'leaflet'
import { Symbol } from 'milsymbol'
export interface MarkerOptions
{
unitName: string
name: string
human: boolean
coalitionID: number
type: any
}
export interface MarkerData
{
heading: number
speed: number
altitude: number
alive: boolean
}
export class UnitMarker extends L.Marker
{
#unitName: string
#name: string
#human: boolean
#coalitionID: number
#alive: boolean
constructor(options: MarkerOptions)
{
super(new L.LatLng(0, 0), {riseOnHover: true});
this.#unitName = options.unitName
this.#name = options.name
this.#human = options.human
this.#coalitionID = options.coalitionID
this.#alive = true;
var symbol = new Symbol(this.#computeMarkerCode(options), {size: 100});
var img = symbol.asCanvas().toDataURL('image/png');
var icon = new L.DivIcon({
html: `<table class="unit-marker-container" id="container">
<tr>
<td>
<div class="unit-marker-icon" id="icon"><img src="${img}"></div>
<div class="unit-marker-unitName" id="unitName">${this.#unitName}</div>
<div class="unit-marker-altitude" id="altitude"></div>
<div class="unit-marker-speed" id="speed"></div>
<div class="unit-marker-name" id="name">${this.#name}</div>
</td>
</tr>
</table>`,
className: 'unit-marker'});
this.setIcon(icon);
}
onAdd(map: L.Map): this
{
super.onAdd(map);
var element = <HTMLElement>this.getElement();
this.addEventListener('mouseover', function(e: any) { e.target?.setHovered(true);});
this.addEventListener('mouseout', function(e: any) { e.target?.setHovered(false);});
return this
}
draw(data: MarkerData)
{
var element = this.getElement();
if (element != null)
{
var nameDiv = <HTMLElement>element.querySelector("#name");
var unitNameDiv = <HTMLElement>element.querySelector("#unitName");
var container = <HTMLElement>element.querySelector("#container");
var icon = <HTMLElement>element.querySelector("#icon");
var altitudeDiv = <HTMLElement>element.querySelector("#altitude");
var speedDiv = <HTMLElement>element.querySelector("#speed");
nameDiv.style.left = (-(nameDiv.offsetWidth - container.offsetWidth) / 2) + "px";
unitNameDiv.style.left = (-(unitNameDiv.offsetWidth - container.offsetWidth) / 2) + "px";
icon.style.transform = "rotate(" + data.heading + "rad)";
altitudeDiv.innerHTML = String(Math.round(data.altitude / 0.3048 / 100) / 10);
speedDiv.innerHTML = String(Math.round(data.speed * 1.94384));
}
}
setSelected(selected: boolean)
{
this.getElement()?.querySelector("#icon")?.classList.remove("unit-marker-hovered");
this.getElement()?.querySelector("#icon")?.classList.toggle("unit-marker-selected", selected);
}
setHovered(hovered: boolean)
{
this.getElement()?.querySelector("#icon")?.classList.toggle("unit-marker-hovered", hovered && this.#alive);
}
#computeMarkerCode(options: MarkerOptions)
{
var identity = "00";
var set = "00";
var entity = "00";
var entityType = "00";
var entitySubtype = "00";
/* Identity */
if (options.coalitionID == 1)
identity = "06" /* Hostile */
else if (options.coalitionID == 2)
identity = "03" /* Friendly */
else
identity = "04" /* Neutral */
if (options.type.level1 == 1)
{
set = "01"
entity = "11"
if (options.type.level2 == 1)
entityType = "01"
else if (options.type.level2 == 1)
entityType = "02"
if (options.type.level3 == 1)
entitySubtype = "04";
else if (options.type.level3 == 2)
entitySubtype = "05";
else if (options.type.level3 == 3)
entitySubtype = "04";
else if (options.type.level3 == 4)
entitySubtype = "02";
else if (options.type.level3 == 5)
entitySubtype = "00";
else if (options.type.level3 == 6)
entitySubtype = "00";
}
else if (options.type.level1 == 2)
set = "10"
else if (options.type.level1 == 3)
set = "30"
else if (options.type.level1 == 2)
set = "02"
return `10${identity}${set}0000${entity}${entityType}${entitySubtype}0000`
}
}

View File

@@ -0,0 +1,260 @@
import { getUnitInfoPanel } from "..";
import { Unit, GroundUnit } from "./unit";
export class UnitsManager
{
#units: { [ID: number]: Unit};
#copiedUnits: Unit[];
constructor()
{
this.#units = {};
this.#copiedUnits = [];
}
addUnit(ID: number, data: any)
{
// The name of the unit category is exactly the same as the constructor name
var constructor = Unit.getConstructor(data.category);
if (constructor != undefined)
{
var options = {
unitName: data.unitName,
name: data.name,
human: data.human,
coalitionID: data.coalitionID,
type: data.type
}
this.#units[ID] = new constructor(ID, options);
}
}
getUnitByID(ID: number)
{
return this.#units[ID];
}
removeUnit(ID: number)
{
}
deselectAllUnits()
{
for (let ID in this.#units)
{
this.#units[ID].setSelected(false);
}
}
update(data: any)
{
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)].update(data["units"][ID]);
}
if (this.getSelectedUnits().length == 1)
{
getUnitInfoPanel().show();
getUnitInfoPanel().update(this.getSelectedUnits()[0]);
}
else
{
getUnitInfoPanel().hide();
}
}
onUnitSelection()
{
//if (this.getSelectedUnits().length > 0)
//{
// map.setState("MOVE_UNIT");
// unitControlPanel.setEnabled(true);
//}
//else
//{
// map.setState("IDLE");
// unitControlPanel.setEnabled(false);
//}
}
// selectFromBounds(bounds)
// {
// this.deselectAllUnits();
// for (let ID in this.#units)
// {
// var latlng = new LatLng(this.#units[ID].latitude, this.#units[ID].longitude);
// if (bounds.contains(latlng))
// {
// this.#units[ID].setSelected(true);
// }
// }
// }
getSelectedUnits()
{
var selectedUnits = [];
for (let ID in this.#units)
{
if (this.#units[ID].getSelected())
{
selectedUnits.push(this.#units[ID]);
}
}
return selectedUnits;
}
// addDestination(latlng)
// {
// var selectedUnits = this.getSelectedUnits();
// for (let idx in selectedUnits)
// {
// var commandedUnit = selectedUnits[idx];
// if (selectedUnits[idx].wingman)
// {
// commandedUnit = this.getLeader(selectedUnits[idx].ID);
// }
// commandedUnit.addDestination(latlng);
// }
// }
// clearDestinations()
// {
// var selectedUnits = this.getSelectedUnits();
// for (let idx in selectedUnits)
// {
// var commandedUnit = selectedUnits[idx];
// if (selectedUnits[idx].wingman)
// {
// commandedUnit = this.getLeader(selectedUnits[idx].ID);
// }
// commandedUnit.clearDestinations();
// }
// }
// selectedUnitsMove()
// {
// }
// selectedUnitsChangeSpeed(speedChange)
// {
// var selectedUnits = this.getSelectedUnits();
// for (let idx in selectedUnits)
// {
// selectedUnits[idx].changeSpeed(speedChange);
// }
// }
// selectedUnitsChangeAltitude(altitudeChange)
// {
// var selectedUnits = this.getSelectedUnits();
// for (let idx in selectedUnits)
// {
// selectedUnits[idx].changeAltitude(altitudeChange);
// }
// }
// handleKeyEvent(e)
// {
// if (e.originalEvent.code === 'KeyC' && e.originalEvent.ctrlKey)
// {
// this.copyUnits();
// }
// else if (e.originalEvent.code === 'KeyV' && e.originalEvent.ctrlKey)
// {
// this.pasteUnits();
// }
// }
// copyUnits()
// {
// this.#copiedUnits = this.getSelectedUnits();
// }
// pasteUnits()
// {
// for (let idx in this.#copiedUnits)
// {
// var unit = this.#copiedUnits[idx];
// cloneUnit(unit.ID);
// }
// }
// attackUnit(ID)
// {
// var selectedUnits = this.getSelectedUnits();
// for (let idx in selectedUnits)
// {
// // If a unit is a wingman, send the command to its leader
// var commandedUnit = selectedUnits[idx];
// if (selectedUnits[idx].wingman)
// {
// commandedUnit = this.getLeader(selectedUnits[idx].ID);
// }
// commandedUnit.attackUnit(ID);
// }
// }
// createFormation(ID)
// {
// var selectedUnits = this.getSelectedUnits();
// var wingmenIDs = [];
// for (let idx in selectedUnits)
// {
// if (selectedUnits[idx].wingman)
// {
// showMessage(selectedUnits[idx].unitName + " is already in a formation.");
// return;
// }
// else if (selectedUnits[idx].leader)
// {
// showMessage(selectedUnits[idx].unitName + " is already in a formation.");
// return;
// }
// else
// {
// /* TODO
// if (selectedUnits[idx].category !== this.getUnitByID(ID).category)
// {
// showMessage("All units must be of the same category to create a formation.");
// }
// */
// if (selectedUnits[idx].ID != ID)
// {
// wingmenIDs.push(selectedUnits[idx].ID);
// }
// }
// }
// if (wingmenIDs.length > 0)
// {
// this.getUnitByID(ID).setLeader(wingmenIDs);
// }
// else
// {
// showMessage("At least 2 units must be selected to create a formation.");
// }
// }
// getLeader(ID)
// {
// for (let idx in this.#units)
// {
// var unit = this.#units[idx];
// if (unit.leader)
// {
// if (unit.wingmen.includes(this.getUnitByID(ID)))
// {
// return unit;
// }
// }
// }
// showMessage("Error: no leader found for this unit")
// }
}