Reworked unit as as state machine, added formations, added measuring on map, added copy-paste

This commit is contained in:
Pax1601
2023-02-01 21:38:36 +01:00
parent bb7259d908
commit 3c1db67733
74 changed files with 4838 additions and 907 deletions

View File

@@ -1,5 +1,5 @@
import { LatLng } from "leaflet";
import { setActiveCoalition } from "..";
import { getActiveCoalition, setActiveCoalition } from "..";
export class SelectionScroll {
#container: HTMLElement | null;
@@ -15,14 +15,17 @@ export class SelectionScroll {
}
}
show(x: number, y: number, options: any, callback: CallableFunction, showCoalition: boolean) {
show(x: number, y: number, title: string, options: any, callback: CallableFunction, showCoalition: boolean) {
/* Hide to remove buttons, if present */
this.hide();
if (this.#container != null && options.length >= 1) {
var titleDiv = this.#container.querySelector("#olympus-selection-scroll-top-bar")?.querySelector(".olympus-selection-scroll-title");
if (titleDiv)
titleDiv.innerHTML = title;
this.#container.style.display = this.#display;
this.#container.style.left = x - 110 + "px";
this.#container.style.top = y - 110 + "px";
this.#container.style.left = x - this.#container.offsetWidth / 2 + "px";
this.#container.style.top = y - 20 + "px";
var scroll = this.#container.querySelector(".olympus-selection-scroll");
if (scroll != null)
{
@@ -40,6 +43,20 @@ export class SelectionScroll {
scroll.appendChild(node);
}
}
/* Hide the coalition switch if required */
var switchContainer = <HTMLElement>this.#container.querySelector("#olympus-selection-scroll-top-bar")?.querySelector("#coalition-switch-container");
if (showCoalition == false) {
switchContainer.style.display = "none";
document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--neutral-coalition-color"));
}
else {
switchContainer.style.display = "block";
if (getActiveCoalition() == "blue")
document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--blue-coalition-color"));
else
document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--red-coalition-color"));
}
}
}

View File

@@ -89,7 +89,7 @@ export function attackUnit(ID: number, targetID: number) {
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " attack " + getUnitsManager().getUnitByID(targetID).unitName);
//console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " attack " + getUnitsManager().getUnitByID(targetID).unitName);
}
};
@@ -105,7 +105,7 @@ export function cloneUnit(ID: number) {
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned");
//console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned");
}
};
@@ -121,7 +121,7 @@ export function changeSpeed(ID: number, speedChange: string) {
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange);
//console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange);
}
};
@@ -137,12 +137,28 @@ export function changeAltitude(ID: number, altitudeChange: string) {
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log(getUnitsManager().getUnitByID(ID).unitName + " altitude change request: " + altitudeChange);
//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));
}
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));
}

View File

@@ -8,19 +8,24 @@ import { Dropdown } from "./controls/dropdown";
import { ConnectionStatusPanel } from "./panels/connectionstatuspanel";
import { Button } from "./controls/button";
import { MissionData } from "./missiondata/missiondata";
import { UnitControlPanel } from "./panels/unitcontrolpanel";
import { MouseInfoPanel } from "./panels/mouseInfoPanel";
/* TODO: should this be a class? */
var map: Map;
var selectionWheel: SelectionWheel;
var selectionScroll: SelectionScroll;
var unitsManager: UnitsManager;
var missionData: MissionData;
var unitInfoPanel: UnitInfoPanel;
var activeCoalition: string;
var connectionStatusPanel: ConnectionStatusPanel;
var unitControlPanel: UnitControlPanel;
var mouseInfoPanel: MouseInfoPanel;
var scenarioDropdown: Dropdown;
var mapSourceDropdown: Dropdown;
var connected: boolean;
var connectionStatusPanel: ConnectionStatusPanel;
var missionData: MissionData;
var slowButton: Button;
var fastButton: Button;
@@ -31,6 +36,9 @@ var aiVisibilityButton: Button;
var weaponVisibilityButton: Button;
var deadVisibilityButton: Button;
var connected: boolean;
var activeCoalition: string;
function setup() {
/* Initialize */
map = new Map('map-container');
@@ -38,9 +46,11 @@ function setup() {
selectionScroll = new SelectionScroll("selection-scroll");
unitsManager = new UnitsManager();
unitInfoPanel = new UnitInfoPanel("unit-info-panel");
unitControlPanel = new UnitControlPanel("unit-control-panel");
scenarioDropdown = new Dropdown("scenario-dropdown", ["Caucasus", "Syria", "Marianas", "Nevada", "South Atlantic", "The channel"], () => { });
mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option));
connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel");
mouseInfoPanel = new MouseInfoPanel("mouse-info-panel");
missionData = new MissionData();
/* Unit control buttons */
@@ -82,6 +92,10 @@ export function getMap() {
return map;
}
export function getMissionData() {
return missionData;
}
export function getSelectionWheel() {
return selectionWheel;
}
@@ -98,6 +112,14 @@ export function getUnitInfoPanel() {
return unitInfoPanel;
}
export function getUnitControlPanel() {
return unitControlPanel;
}
export function getMouseInfoPanel() {
return mouseInfoPanel;
}
export function setActiveCoalition(newActiveCoalition: string) {
activeCoalition = newActiveCoalition;
}

View File

@@ -45,7 +45,7 @@ export var BoxSelect = Handler.extend({
},
_onMouseDown: function (e: any) {
if (((e.which !== 3) && (e.button !== 2))) { return false; }
if (((e.which !== 1) && (e.button !== 0))) { return false; }
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
@@ -107,7 +107,7 @@ export var BoxSelect = Handler.extend({
},
_onMouseUp: function (e: any) {
if ((e.which !== 3) && (e.button !== 2)) { return; }
if ((e.which !== 1) && (e.button !== 0)) { return; }
this._finish();

View File

@@ -1,5 +1,5 @@
import * as L from "leaflet"
import { getSelectionWheel, getSelectionScroll, getUnitsManager, getActiveCoalition } from "..";
import { getSelectionWheel, getSelectionScroll, getUnitsManager, getActiveCoalition, getMouseInfoPanel } from "..";
import { spawnAircraft, spawnGroundUnit, spawnSmoke } from "../dcs/dcs";
import { payloadNames } from "../units/payloadNames";
import { unitTypes } from "../units/unitTypes";
@@ -21,8 +21,11 @@ export interface SpawnEvent extends ClickEvent{
export class Map extends L.Map {
#state: string;
#layer?: L.TileLayer;
#preventRightClick: boolean = false;
#rightClickTimer: number = 0;
#preventLeftClick: boolean = false;
#leftClickTimer: number = 0;
#measurePoint: L.LatLng | null;
#measureIcon: L.Icon;
#measureMarker: L.Marker;
constructor(ID: string) {
/* Init the leaflet map */
@@ -34,12 +37,19 @@ export class Map extends L.Map {
/* Init the state machine */
this.#state = "IDLE";
this.#measurePoint = null;
this.#measureIcon = new L.Icon({ iconUrl: 'images/pin.png', iconAnchor: [16, 32]});
this.#measureMarker = new L.Marker([0, 0], {icon: this.#measureIcon});
/* Register event handles */
this.on("click", (e: any) => this.#onClick(e));
this.on("dblclick", (e: any) => this.#onDoubleClick(e));
this.on("contextmenu", (e: any) => this.#onContextMenu(e));
this.on('selectionend', (e: any) => this.#onSelectionEnd(e));
this.on('mousedown', (e: any) => this.#onMouseDown(e));
this.on('mouseup', (e: any) => this.#onMouseUp(e));
this.on('mousemove', (e: any) => this.#onMouseMove(e));
}
setLayer(layerName: string) {
@@ -124,10 +134,10 @@ export class Map extends L.Map {
}
/* Selection scroll */
showSelectionScroll(e: ClickEvent | SpawnEvent, options: any, callback: CallableFunction, showCoalition: boolean = false) {
showSelectionScroll(e: ClickEvent | SpawnEvent, title: string, options: any, callback: CallableFunction, showCoalition: boolean = false) {
var x = e.x;
var y = e.y;
getSelectionScroll().show(x, y, options, callback, showCoalition);
getSelectionScroll().show(x, y, title, options, callback, showCoalition);
}
hideSelectionScroll() {
@@ -136,51 +146,97 @@ export class Map extends L.Map {
/* Event handlers */
#onClick(e: any) {
this.hideSelectionWheel();
this.hideSelectionScroll();
if (this.#state === "IDLE") {
}
else if (this.#state === "MOVE_UNIT") {
if (!e.originalEvent.ctrlKey) {
getUnitsManager().clearDestinations();
if (!this.#preventLeftClick) {
this.hideSelectionWheel();
this.hideSelectionScroll();
if (this.#state === "IDLE") {
if (e.originalEvent.ctrlKey)
if (!this.#measurePoint)
{
this.#measurePoint = e.latlng;
this.#measureMarker.setLatLng(e.latlng);
this.#measureMarker.addTo(this);
}
else
{
this.#measurePoint = null;
if (this.hasLayer(this.#measureMarker))
this.removeLayer(this.#measureMarker);
}
}
getUnitsManager().addDestination(e.latlng)
}
}
#onDoubleClick(e: any) {
var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: null, coalitionID: null};
if (this.#state == "IDLE") {
var options = [
{ "tooltip": "Spawn air unit", "src": "spawnAir.png", "callback": () => this.#aircraftSpawnMenu(spawnEvent) },
{ "tooltip": "Spawn ground unit", "src": "spawnGround.png", "callback": () => this.#groundUnitSpawnMenu(spawnEvent) },
{ "tooltip": "Smoke", "src": "spawnSmoke.png", "callback": () => this.#smokeSpawnMenu(spawnEvent) },
//{ "tooltip": "Explosion", "src": "spawnExplosion.png", "callback": () => this.#explosionSpawnMenu(e) }
]
this.showSelectionScroll(spawnEvent, options, () => {}, true);
}
}
#onContextMenu(e: any) {
this.#rightClickTimer = setTimeout(() => {
if (!this.#preventRightClick) {
else if (this.#state === "MOVE_UNIT") {
this.setState("IDLE");
getUnitsManager().deselectAllUnits();
this.hideSelectionWheel();
this.hideSelectionScroll();
}
this.#preventRightClick = false;
}, 200);
}
}
#onDoubleClick(e: any) {
}
#onContextMenu(e: any) {
this.hideSelectionWheel();
this.hideSelectionScroll();
if (this.#state === "IDLE") {
var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: null, coalitionID: null};
if (this.#state == "IDLE") {
var options = [
{ "tooltip": "Spawn air unit", "src": "spawnAir.png", "callback": () => this.#aircraftSpawnMenu(spawnEvent) },
{ "tooltip": "Spawn ground unit", "src": "spawnGround.png", "callback": () => this.#groundUnitSpawnMenu(spawnEvent) },
{ "tooltip": "Smoke", "src": "spawnSmoke.png", "callback": () => this.#smokeSpawnMenu(spawnEvent) },
//{ "tooltip": "Explosion", "src": "spawnExplosion.png", "callback": () => this.#explosionSpawnMenu(e) }
]
this.showSelectionScroll(spawnEvent, "Action", options, () => {}, false);
}
}
else if (this.#state === "MOVE_UNIT") {
if (!e.originalEvent.ctrlKey) {
getUnitsManager().selectedUnitsClearDestinations();
}
getUnitsManager().selectedUnitsAddDestination(e.latlng)
}
}
#onSelectionEnd(e: any)
{
clearTimeout(this.#rightClickTimer);
this.#preventRightClick = true;
clearTimeout(this.#leftClickTimer);
this.#preventLeftClick = true;
this.#leftClickTimer = setTimeout(() => {
this.#preventLeftClick = false;
}, 200);
getUnitsManager().selectFromBounds(e.selectionBounds);
}
#onMouseDown(e: any)
{
if ((e.originalEvent.which == 1) && (e.originalEvent.button == 0))
{
this.dragging.disable();
}
}
#onMouseUp(e: any)
{
if ((e.originalEvent.which == 1) && (e.originalEvent.button == 0))
{
this.dragging.enable();
}
}
#onMouseMove(e: any)
{
var selectedUnitPosition = null;
var selectedUnits = getUnitsManager().getSelectedUnits();
if (selectedUnits && selectedUnits.length == 1)
{
selectedUnitPosition = new L.LatLng(selectedUnits[0].latitude, selectedUnits[0].longitude);
}
getMouseInfoPanel().update(<L.LatLng>e.latlng, this.#measurePoint, selectedUnitPosition);
}
/* Spawn from air base */
spawnFromAirbase(e: SpawnEvent)
{
@@ -198,7 +254,7 @@ export class Map extends L.Map {
{'coalition': true, 'tooltip': 'Radar', 'src': 'spawnRadar.png', 'callback': () => this.#selectGroundUnit(e, "Radar")},
{'coalition': true, 'tooltip': 'Unarmed', 'src': 'spawnUnarmed.png', 'callback': () => this.#selectGroundUnit(e, "Unarmed")}
]
this.showSelectionScroll(e, options, () => {}, true);
this.showSelectionScroll(e, "Spawn ground unit", options, () => {}, true);
}
#smokeSpawnMenu(e: SpawnEvent) {
@@ -211,7 +267,7 @@ export class Map extends L.Map {
{'tooltip': 'Green smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('green', e.latlng)}, 'tint': 'green'},
{'tooltip': 'Orange smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('orange', e.latlng)}, 'tint': 'orange'},
]
this.showSelectionScroll(e, options, () => {}, true);
this.showSelectionScroll(e, "Spawn smoke", options, () => {}, false);
}
#explosionSpawnMenu(e: SpawnEvent) {
@@ -228,7 +284,10 @@ export class Map extends L.Map {
{ '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.showSelectionScroll(e, options, () => {}, true);
if (e.airbaseName != null)
this.showSelectionScroll(e, "Spawn at " + e.airbaseName, options, () => {}, true);
else
this.showSelectionScroll(e, "Spawn air unit", options, () => {}, true);
}
/* Show unit selection for air units */
@@ -240,11 +299,11 @@ export class Map extends L.Map {
options.sort();
else
options = [];
this.showSelectionScroll(e, options, (unitType: string) => {
this.showSelectionScroll(e, "Select aircraft", options, (unitType: string) => {
this.hideSelectionWheel();
this.hideSelectionScroll();
this.#unitSelectPayload(e, unitType);
});
}, true);
}
/* Show weapon selection for air units */
@@ -255,11 +314,11 @@ export class Map extends L.Map {
options = payloadNames[unitType]
if (options != undefined && options.length > 0) {
options.sort();
this.showSelectionScroll({x: e.x, y: e.y, latlng: e.latlng}, options, (payloadName: string) => {
this.showSelectionScroll({x: e.x, y: e.y, latlng: e.latlng}, "Select loadout", options, (payloadName: string) => {
this.hideSelectionWheel();
this.hideSelectionScroll();
spawnAircraft(unitType, e.latlng, getActiveCoalition(), payloadName, e.airbaseName);
});
}, true);
}
else {
spawnAircraft(unitType, e.latlng, getActiveCoalition());
@@ -273,10 +332,10 @@ export class Map extends L.Map {
this.hideSelectionScroll();
var options = unitTypes.vehicles[group];
options.sort();
this.showSelectionScroll(e, options, (unitType: string) => {
this.showSelectionScroll(e, "Select ground unit", options, (unitType: string) => {
this.hideSelectionWheel();
this.hideSelectionScroll();
spawnGroundUnit(unitType, e.latlng, getActiveCoalition());
});
}, true);
}
}

View File

@@ -1,41 +1,56 @@
import { Marker, LatLng } from "leaflet";
import { getMap } from "..";
import { Marker, LatLng, Icon } from "leaflet";
import { getMap, getUnitsManager } from "..";
import { SpawnEvent } from "../map/map";
import { AirbaseMarker } from "./airbasemarker";
var bullseyeIcons = [
new Icon({ iconUrl: 'images/bullseye0.png', iconAnchor: [30, 30]}),
new Icon({ iconUrl: 'images/bullseye1.png', iconAnchor: [30, 30]}),
new Icon({ iconUrl: 'images/bullseye2.png', iconAnchor: [30, 30]})
]
export class MissionData
{
//#bullseye : any; //TODO declare interface
//#bullseyeMarker : Marker;
#bullseyes : any; //TODO declare interface
#bullseyeMarkers: any;
#airbases : any; //TODO declare interface
#airbasesMarkers: {[name: string]: AirbaseMarker};
constructor()
{
//this.#bullseye = undefined;
//this.#bullseyeMarker = undefined;
this.#bullseyes = undefined;
this.#bullseyeMarkers = [
new Marker([0, 0], {icon: bullseyeIcons[0]}).addTo(getMap()),
new Marker([0, 0], {icon: bullseyeIcons[1]}).addTo(getMap()),
new Marker([0, 0], {icon: bullseyeIcons[2]}).addTo(getMap())
]
this.#airbasesMarkers = {};
}
update(data: any)
{
//this.#bullseye = data.missionData.bullseye;
this.#bullseyes = data.bullseye;
this.#airbases = data.airbases;
//this.#drawBullseye();
this.#drawAirbases();
if (this.#bullseyes != null && this.#airbases != null)
{
this.#drawBullseye();
this.#drawAirbases();
}
}
//#drawBullseye()
//{
// if (this.#bullseyeMarker === undefined)
// {
// this.#bullseyeMarker = new Marker([this.#bullseye.lat, this.#bullseye.lng]).addTo(map.getMap());
// }
// else
// {
// this.#bullseyeMarker.setLatLng(new LatLng(this.#bullseye.lat, this.#bullseye.lng));
// }
//}
getBullseyes()
{
return this.#bullseyes;
}
#drawBullseye()
{
for (let idx in this.#bullseyes)
{
var bullseye = this.#bullseyes[idx];
this.#bullseyeMarkers[idx].setLatLng(new LatLng(bullseye.lat, bullseye.lng));
}
}
#drawAirbases()
{
@@ -48,7 +63,7 @@ export class MissionData
position: new LatLng(airbase.lat, airbase.lng),
name: airbase.callsign,
src: "images/airbase.png"}).addTo(getMap());
this.#airbasesMarkers[idx].on('click', (e) => this.#onAirbaseClick(e));
this.#airbasesMarkers[idx].on('contextmenu', (e) => this.#onAirbaseClick(e));
}
else
{
@@ -59,7 +74,25 @@ export class MissionData
#onAirbaseClick(e: any)
{
var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: e.sourceTarget.getName(), coalitionID: e.sourceTarget.getCoalitionID()};
getMap().spawnFromAirbase(spawnEvent);
var options = [];
if (getUnitsManager().getSelectedUnits().length > 0)
options = ["Spawn unit", "Land here"];
else
options = ["Spawn unit"];
getMap().showSelectionScroll(e.originalEvent, e.sourceTarget.getName(), options, (option: string) => this.#onAirbaseOptionSelection(e, option), false);
}
#onAirbaseOptionSelection(e: any, option: string) {
if (option === "Spawn unit") {
var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: e.sourceTarget.getName(), coalitionID: e.sourceTarget.getCoalitionID()};
getMap().spawnFromAirbase(spawnEvent);
}
else if (option === "Land here")
{
getMap().hideSelectionWheel();
getMap().hideSelectionScroll();
getUnitsManager().selectedUnitsLandAt(e.latlng);
}
}
}

View File

@@ -26,7 +26,7 @@ export function bearing(lat1: number, lon1: number, lat2: number, lon2: number)
return brng;
}
const zeroPad = function (num: number, places: number) {
export const zeroPad = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
string += "0";
@@ -34,6 +34,14 @@ const zeroPad = function (num: number, places: number) {
return string;
}
export const zeroAppend = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
string = "0" + string;
}
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);

View File

@@ -0,0 +1,73 @@
import { LatLng } from "leaflet";
import { getMissionData } from "..";
import { distance, bearing, zeroPad, zeroAppend } from "../other/utils";
import { Unit } from "../units/unit";
export class MouseInfoPanel {
#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;
var el = <HTMLElement>this.#element.querySelector(`#measure-position`);
this.show();
}
}
show() {
this.#element.style.display = this.#display;
}
hide() {
this.#element.style.display = "none";
}
update(mousePosition: LatLng, measurePosition: LatLng | null, unitPosition: LatLng | null) {
var bullseyes = getMissionData().getBullseyes();
for (let idx in bullseyes)
{
var dist = distance(bullseyes[idx].lat, bullseyes[idx].lng, mousePosition.lat, mousePosition.lng);
var bear = bearing(bullseyes[idx].lat, bullseyes[idx].lng, mousePosition.lat, mousePosition.lng);
var el = <HTMLElement>this.#element.querySelector(`#bullseye-${idx}`);
if (el != null)
el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM`
}
if (measurePosition) {
var dist = distance(measurePosition.lat, measurePosition.lng, mousePosition.lat, mousePosition.lng);
var bear = bearing(measurePosition.lat, measurePosition.lng, mousePosition.lat, mousePosition.lng);
var el = <HTMLElement>this.#element.querySelector(`#measure-position`);
if (el != null)
{
el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM`
if (el.parentElement != null)
el.parentElement.style.display = 'flex'; //TODO: don't like that its hardcoded
}
}
else {
var el = <HTMLElement>this.#element.querySelector(`#measure-position`);
if (el != null && el.parentElement != null)
el.parentElement.style.display = 'none';
}
if (unitPosition) {
var dist = distance(unitPosition.lat, unitPosition.lng, mousePosition.lat, mousePosition.lng);
var bear = bearing(unitPosition.lat, unitPosition.lng, mousePosition.lat, mousePosition.lng);
var el = <HTMLElement>this.#element.querySelector(`#unit-position`);
if (el != null)
{
el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM`
if (el.parentElement != null)
el.parentElement.style.display = 'flex'; //TODO: don't like that its hardcoded
}
}
else {
var el = <HTMLElement>this.#element.querySelector(`#unit-position`);
if (el != null && el.parentElement != null)
el.parentElement.style.display = 'none';
}
}
}

View File

@@ -0,0 +1,185 @@
import { imageOverlay } from "leaflet";
import { getUnitsManager } from "..";
import { ConvertDDToDMS, rad2deg } from "../other/utils";
import { AirUnit, Unit } from "../units/unit";
export class UnitControlPanel {
#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;
var formationCreationContainer = <HTMLElement>(this.#element.querySelector("#formation-creation-container"));
if (formationCreationContainer != null)
{
var createButton = <HTMLElement>formationCreationContainer.querySelector("#create-formation");
createButton?.addEventListener("click", () => getUnitsManager().selectedUnitsCreateFormation());
var undoButton = <HTMLElement>formationCreationContainer.querySelector("#undo-formation");
undoButton?.addEventListener("click", () => getUnitsManager().selectedUnitsUndoFormation());
}
this.hide();
}
}
show() {
this.#element.style.display = this.#display;
}
hide() {
this.#element.style.display = "none";
}
update(units: Unit[]) {
if (this.#element != null)
{
var selectedUnitsContainer = <HTMLElement>(this.#element.querySelector("#selected-units-container"));
var formationCreationContainer = <HTMLElement>(this.#element.querySelector("#formation-creation-container"));
if (selectedUnitsContainer != null && formationCreationContainer != null)
{
this.#addUnitsButtons(units, selectedUnitsContainer);
this.#showFormationButtons(units, formationCreationContainer);
}
}
}
#addUnitsButtons(units: Unit[], selectedUnitsContainer: HTMLElement)
{
/* Remove any pre-existing unit button */
var elements = selectedUnitsContainer.getElementsByClassName("js-unit-container");
while (elements.length > 0)
selectedUnitsContainer.removeChild(elements[0])
/* Create all the units buttons */
for (let unit of units)
{
this.#addUnitButton(unit, selectedUnitsContainer);
if (unit.isLeader)
for (let wingman of unit.getWingmen())
this.#addUnitButton(wingman, selectedUnitsContainer);
}
}
#addUnitButton(unit: Unit, container: HTMLElement)
{
var el = document.createElement("div");
/* Unit name (actually type, but DCS calls it name for some reason) */
var nameDiv = document.createElement("div");
nameDiv.classList.add("rounded-container-small");
if (unit.name.length >= 7)
nameDiv.innerHTML = `${unit.name.substring(0, 4)} ...`;
else
nameDiv.innerHTML = `${unit.name}`;
/* Unit icon */
var icon = document.createElement("img");
if (unit.isLeader)
icon.src = "images/icons/formation.png"
else if (unit.isWingman)
{
var wingmen = unit.getLeader()?.getWingmen();
if (wingmen && wingmen.lastIndexOf(unit) == wingmen.length - 1)
icon.src = "images/icons/formation-end.svg"
else
icon.src = "images/icons/formation-middle.svg"
}
else
icon.src = "images/icons/singleton.png"
el.innerHTML = unit.unitName;
el.prepend(nameDiv);
/* Show the icon only for air units */
if ((unit instanceof AirUnit))
el.append(icon);
el.classList.add("rounded-container", "js-unit-container");
if (!unit.getSelected())
el.classList.add("not-selected")
/* Set background color */
if (unit.coalitionID == 1)
{
el.classList.add("red");
icon.classList.add("red");
}
else if (unit.coalitionID == 2)
{
el.classList.add("blue");
icon.classList.add("blue");
}
else
{
el.classList.add("neutral");
icon.classList.add("neutral");
}
el.addEventListener("click", () => getUnitsManager().selectUnit(unit.ID));
container.appendChild(el);
}
#showFormationButtons(units: Unit[], formationCreationContainer: HTMLElement)
{
var createButton = <HTMLElement>formationCreationContainer.querySelector("#create-formation");
var undoButton = <HTMLElement>formationCreationContainer.querySelector("#undo-formation");
if (createButton && undoButton && this.#checkAllUnitsAir(units))
{
if (!this.#checkUnitsAlreadyInFormation(units))
{
createButton.style.display = '';
undoButton.style.display = 'none';
}
else if (this.#checkUnitsAlreadyInFormation(units) && this.#checkAllUnitsSameFormation(units))
{
createButton.style.display = 'none';
undoButton.style.display = '';
}
else
{
createButton.style.display = 'none';
undoButton.style.display = 'none';
}
}
}
#checkAllUnitsAir(units: Unit[])
{
for (let unit of units)
if (!(unit instanceof AirUnit))
return false
return true
}
#checkAllUnitsSameFormation(units: Unit[])
{
var leaderFound = false;
for (let unit of units)
{
if (unit.isLeader)
{
if (leaderFound)
return false
else
leaderFound = true;
}
if (!unit.isLeader)
return false
}
return true
}
#checkUnitsAlreadyInFormation(units: Unit[])
{
for (let unit of units)
if (unit.isLeader)
return true
return false
}
}

View File

@@ -48,6 +48,15 @@ export class UnitInfoPanel {
this.#element.querySelector("#latitude")!.innerHTML = ConvertDDToDMS(unit.latitude, false);
this.#element.querySelector("#longitude")!.innerHTML = ConvertDDToDMS(unit.longitude, true);
this.#element.querySelector("#task")!.innerHTML = unit.currentTask !== ""? unit.currentTask: "Not controlled";
this.#element.querySelector("#task")!.classList.remove("red", "blue", "neutral");
if (unit.coalitionID == 1)
this.#element.querySelector("#task")!.classList.add("red");
else if (unit.coalitionID == 2)
this.#element.querySelector("#task")!.classList.add("blue");
else
this.#element.querySelector("#task")!.classList.add("neutral");
}
}
}

View File

@@ -2,7 +2,7 @@ import { Marker, LatLng, Polyline, Icon } from 'leaflet';
import { ConvertDDToDMS } from '../other/utils';
import { getMap, getUnitsManager, getVisibilitySettings } from '..';
import { UnitMarker, MarkerOptions, AircraftMarker, HelicopterMarker, GroundUnitMarker, NavyUnitMarker, WeaponMarker } from './unitmarker';
import { addDestination, attackUnit, changeAltitude, changeSpeed } from '../dcs/dcs';
import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader } from '../dcs/dcs';
var pathIcon = new Icon({
iconUrl: 'images/marker-icon.png',
@@ -12,9 +12,6 @@ var pathIcon = new Icon({
export class Unit {
ID: number = -1;
leader: boolean = false;
wingman: boolean = false;
wingmen: Unit[] = [];
formation: string = "";
name: string = "";
unitName: string = "";
@@ -33,7 +30,13 @@ export class Unit {
activePath: any = null;
ammo: any = null;
targets: any = null;
hasTask: boolean = false;
isLeader: boolean = false;
isWingman: boolean = false;
leaderID: number = 0;
wingmen: Unit[] = [];
wingmenIDs: number[] = [];
#selectable: boolean;
#selected: boolean = false;
#preventClick: boolean = false;
@@ -61,42 +64,36 @@ export class Unit {
this.#marker = marker;
this.#marker.on('click', (e) => this.#onClick(e));
this.#marker.on('dblclick', (e) => this.#onDoubleClick(e));
this.#marker.on('contextmenu', (e) => this.#onContextMenu(e));
this.#pathPolyline = new Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1 });
this.#pathPolyline.addTo(getMap());
this.#targetsPolylines = [];
}
update(response: JSON) {
update(response: any) {
for (let entry in response) {
// @ts-ignore
// @ts-ignore TODO handle better
this[entry] = response[entry];
}
// TODO handle better
if (response['activePath'] == undefined)
this.activePath = null
/* Dead units can't be selected */
this.setSelected(this.getSelected() && this.alive)
this.#updateMarker();
this.#clearTargets();
if (this.getSelected())
if (this.getSelected() && this.activePath != null)
{
this.#drawPath();
this.#drawTargets();
}
else
this.#clearPath();
/*
this.wingmen = [];
if (response["wingmenIDs"] != null)
{
for (let ID of response["wingmenIDs"])
{
this.wingmen.push(unitsManager.getUnitByID(ID));
}
}
*/
this.#clearPath();
}
setSelected(selected: boolean) {
@@ -105,6 +102,7 @@ export class Unit {
this.#selected = selected;
this.#marker.setSelected(selected);
getUnitsManager().onUnitSelection();
}
}
@@ -140,6 +138,28 @@ export class Unit {
return false;
}
getLeader() {
return getUnitsManager().getUnitByID(this.leaderID);
}
getFormation() {
return [<Unit>this].concat(this.getWingmen())
}
getWingmen() {
var wingmen: Unit[] = [];
if (this.wingmenIDs != null)
{
for (let ID of this.wingmenIDs)
{
var unit = getUnitsManager().getUnitByID(ID)
if (unit)
wingmen.push(unit);
}
}
return wingmen;
}
#onClick(e: any) {
this.#timer = setTimeout(() => {
if (!this.#preventClick) {
@@ -157,22 +177,21 @@ export class Unit {
#onDoubleClick(e: any) {
clearTimeout(this.#timer);
this.#preventClick = true;
}
#onContextMenu(e: any) {
var options = [
'Attack',
'Follow'
]
//if (!this.leader && !this.wingman) {
// options.push({ 'tooltip': 'Create formation', 'src': 'formation.png', 'callback': () => { getMap().hideSelectionWheel(); /*unitsManager.createFormation(this.ID);*/ } });
//}
getMap().showSelectionScroll(e.originalEvent, options, (action: string) => this.#executeAction(action));
getMap().showSelectionScroll(e.originalEvent, "Action: " + this.unitName, options, (action: string) => this.#executeAction(action));
}
#executeAction(action: string) {
getMap().hideSelectionScroll();
if (action === "Attack")
getUnitsManager().attackUnit(this.ID);
getUnitsManager().selectedUnitsAttackUnit(this.ID);
}
#updateMarker() {
@@ -318,25 +337,13 @@ export class Unit {
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));
}
*/
setLeader(isLeader: boolean, wingmenIDs: number[] = [])
{
setLeader(this.ID, isLeader, wingmenIDs);
}
}
export class AirUnit extends Unit {

View File

@@ -1,6 +1,7 @@
import { LatLng, LatLngBounds } from "leaflet";
import { getMap, getUnitInfoPanel } from "..";
import { getMap, getUnitControlPanel, getUnitInfoPanel } from "..";
import { Unit, GroundUnit } from "./unit";
import { cloneUnit } from "../dcs/dcs";
export class UnitsManager {
#units: { [ID: number]: Unit };
@@ -9,6 +10,13 @@ export class UnitsManager {
constructor() {
this.#units = {};
this.#copiedUnits = [];
document.addEventListener('copy', () => this.copyUnits());
document.addEventListener('paste', () => this.pasteUnits());
}
getUnits() {
return this.#units;
}
addUnit(ID: number, data: any) {
@@ -27,7 +35,10 @@ export class UnitsManager {
}
getUnitByID(ID: number) {
return this.#units[ID];
if (ID in this.#units)
return this.#units[ID];
else
return null;
}
removeUnit(ID: number) {
@@ -40,6 +51,13 @@ export class UnitsManager {
}
}
selectUnit(ID: number, deselectAllUnits: boolean = true)
{
if (deselectAllUnits)
this.deselectAllUnits();
this.#units[ID]?.setSelected(true);
}
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. */
@@ -49,6 +67,7 @@ export class UnitsManager {
this.#units[parseInt(ID)].update(data["units"][ID]);
}
/* Update the unit info panel */
if (this.getSelectedUnits().length == 1) {
getUnitInfoPanel().show();
getUnitInfoPanel().update(this.getSelectedUnits()[0]);
@@ -67,6 +86,8 @@ export class UnitsManager {
getMap().setState("IDLE");
//unitControlPanel.setEnabled(false);
}
this.#updateUnitControlPanel();
}
selectFromBounds(bounds: LatLngBounds)
@@ -92,7 +113,35 @@ export class UnitsManager {
return selectedUnits;
}
addDestination(latlng: L.LatLng) {
getSelectedLeaders() {
var leaders: Unit[] = [];
for (let idx in this.getSelectedUnits())
{
var unit = this.getSelectedUnits()[idx];
if (unit.isLeader)
leaders.push(unit);
else if (unit.isWingman)
{
var leader = unit.getLeader();
if (leader && !leaders.includes(leader))
leaders.push(leader);
}
}
return leaders;
}
getSelectedSingletons() {
var singletons: Unit[] = [];
for (let idx in this.getSelectedUnits())
{
var unit = this.getSelectedUnits()[idx];
if (!unit.isLeader && !unit.isWingman)
singletons.push(unit);
}
return singletons;
}
selectedUnitsAddDestination(latlng: L.LatLng) {
var selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
var commandedUnit = selectedUnits[idx];
@@ -104,7 +153,7 @@ export class UnitsManager {
}
}
clearDestinations() {
selectedUnitsClearDestinations() {
var selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
var commandedUnit = selectedUnits[idx];
@@ -116,6 +165,11 @@ export class UnitsManager {
}
}
selectedUnitsLandAt(latlng: LatLng)
{
}
selectedUnitsChangeSpeed(speedChange: string)
{
var selectedUnits = this.getSelectedUnits();
@@ -134,33 +188,21 @@ export class UnitsManager {
}
}
// 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();
}
// copyUnits()
// {
// this.#copiedUnits = this.getSelectedUnits();
// }
pasteUnits()
{
for (let idx in this.#copiedUnits)
{
var unit = this.#copiedUnits[idx];
cloneUnit(unit.ID);
}
}
// pasteUnits()
// {
// for (let idx in this.#copiedUnits)
// {
// var unit = this.#copiedUnits[idx];
// cloneUnit(unit.ID);
// }
// }
attackUnit(ID: number) {
selectedUnitsAttackUnit(ID: number) {
var selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
/* If a unit is a wingman, send the command to its leader */
@@ -173,59 +215,70 @@ export class UnitsManager {
}
}
// 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.");
// }
// }
selectedUnitsCreateFormation(ID: number | null = null)
{
var selectedUnits = this.getSelectedUnits();
if (selectedUnits.length >= 2)
{
if (ID == null)
ID = selectedUnits[0].ID
var wingmenIDs = [];
for (let idx in selectedUnits)
{
if (selectedUnits[idx].isWingman)
{
console.log(selectedUnits[idx].unitName + " is already in a formation.");
return;
}
else if (selectedUnits[idx].isLeader)
{
console.log(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(true, wingmenIDs);
}
else
{
console.log("At least 2 units must be selected to create a formation.");
}
}
setTimeout(() => this.#updateUnitControlPanel(), 1000); // TODO find better method, may fail
}
// 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")
// }
selectedUnitsUndoFormation(ID: number | null = null)
{
for (let leader of this.getSelectedLeaders())
{
leader.setLeader(false);
}
setTimeout(() => this.#updateUnitControlPanel(), 1000); // TODO find better method, may fail
}
#updateUnitControlPanel() {
/* Update the unit control panel */
if (this.getSelectedUnits().length > 0) {
getUnitControlPanel().show();
getUnitControlPanel().update(this.getSelectedLeaders().concat(this.getSelectedSingletons()));
}
else {
getUnitControlPanel().hide();
}
}
}