Merge pull request #290 from Pax1601/main

Merge main commits into airfield JSON addition branch
This commit is contained in:
Shredmetal
2023-05-24 18:35:18 +08:00
committed by GitHub
305 changed files with 4932 additions and 5104 deletions

View File

@@ -8,7 +8,7 @@ interface AirbasesData {
}
interface BullseyesData {
bullseyes: {[key: string]: any},
bullseyes: {[key: string]: {latitude: number, longitude: number, coalition: string}},
}
interface LogData {

View File

@@ -79,4 +79,16 @@ interface GeneralSettings {
prohibitAG: boolean;
prohibitAfterburner: boolean;
prohibitAirWpn: boolean;
}
interface UnitIconOptions {
showState: boolean,
showVvi: boolean,
showHotgroup: boolean,
showUnitIcon: boolean,
showShortLabel: boolean,
showFuel: boolean,
showAmmo: boolean,
showSummary: boolean,
rotateToHeading: boolean
}

View File

@@ -1,4 +1,4 @@
import { ToggleableFeature } from "../toggleablefeature";
import { ToggleableFeature } from "../features/toggleablefeature";
import { AICFormation_Azimuth } from "./aicformation/azimuth";
import { AICFormation_Range } from "./aicformation/range";
import { AICFormation_Single } from "./aicformation/single";

View File

@@ -1,6 +1,6 @@
import { getUnitsManager } from "..";
import { Panel } from "../panels/panel";
import { Unit } from "./unit";
import { Unit } from "../units/unit";
export class UnitDataTable extends Panel {
constructor(id: string) {

View File

@@ -5,8 +5,7 @@ import { ContextMenu } from "./contextmenu";
export class AirbaseContextMenu extends ContextMenu {
#airbase: Airbase | null = null;
constructor(id: string)
{
constructor(id: string) {
super(id);
document.addEventListener("contextMenuSpawnAirbase", (e: any) => {
this.showSpawnMenu();
@@ -19,8 +18,7 @@ export class AirbaseContextMenu extends ContextMenu {
})
}
setAirbase(airbase: Airbase)
{
setAirbase(airbase: Airbase) {
this.#airbase = airbase;
this.setName(airbase.getName());
this.setProperties(airbase.getProperties());
@@ -29,24 +27,21 @@ export class AirbaseContextMenu extends ContextMenu {
this.enableLandButton(getUnitsManager().getSelectedUnitsType() === "Aircraft" && (getUnitsManager().getSelectedUnitsCoalition() === airbase.getCoalition() || airbase.getCoalition() === "neutral"))
}
setName(airbaseName: string)
{
setName(airbaseName: string) {
var nameDiv = <HTMLElement>this.getContainer()?.querySelector("#airbase-name");
if (nameDiv != null)
nameDiv.innerText = airbaseName;
nameDiv.innerText = airbaseName;
}
setProperties(airbaseProperties: string[])
{
setProperties(airbaseProperties: string[]) {
this.getContainer()?.querySelector("#airbase-properties")?.replaceChildren(...airbaseProperties.map((property: string) => {
var div = document.createElement("div");
div.innerText = property;
return div;
}), );
}),);
}
setParkings(airbaseParkings: string[])
{
setParkings(airbaseParkings: string[]) {
this.getContainer()?.querySelector("#airbase-parking")?.replaceChildren(...airbaseParkings.map((parking: string) => {
var div = document.createElement("div");
div.innerText = parking;
@@ -54,22 +49,18 @@ export class AirbaseContextMenu extends ContextMenu {
}));
}
setCoalition(coalition: string)
{
setCoalition(coalition: string) {
(<HTMLElement>this.getContainer()?.querySelector("#spawn-airbase-aircraft-button")).dataset.activeCoalition = coalition;
}
enableLandButton(enableLandButton: boolean)
{
enableLandButton(enableLandButton: boolean) {
this.getContainer()?.querySelector("#land-here-button")?.classList.toggle("hide", !enableLandButton);
}
showSpawnMenu()
{
if (this.#airbase != null)
{
showSpawnMenu() {
if (this.#airbase != null) {
setActiveCoalition(this.#airbase.getCoalition());
getMap().showMapContextMenu({originalEvent: {x: this.getX(), y: this.getY(), latlng: this.getLatLng()}});
getMap().showMapContextMenu({ originalEvent: { x: this.getX(), y: this.getY(), latlng: this.getLatLng() } });
getMap().getMapContextMenu().hideUpperBar();
getMap().getMapContextMenu().showSubMenu("aircraft");
getMap().getMapContextMenu().setAirbaseName(this.#airbase.getName());

View File

@@ -23,28 +23,23 @@ export class ContextMenu {
this.#container?.classList.toggle("hide", true);
}
getContainer()
{
getContainer() {
return this.#container;
}
getLatLng()
{
getLatLng() {
return this.#latlng;
}
getX()
{
getX() {
return this.#x;
}
getY()
{
getY() {
return this.#y;
}
clip()
{
clip() {
if (this.#container != null) {
if (this.#x + this.#container.offsetWidth < window.innerWidth)
this.#container.style.left = this.#x + "px";

View File

@@ -7,17 +7,16 @@ export class Dropdown {
#optionsList: string[] = [];
#index: number = 0;
constructor(ID: string, callback: CallableFunction, options: string[] | null = null)
{
this.#element = <HTMLElement>document.getElementById(ID);
this.#options = <HTMLElement>this.#element.querySelector(".ol-select-options");
this.#value = <HTMLElement>this.#element.querySelector(".ol-select-value");
constructor(ID: string, callback: CallableFunction, options: string[] | null = null) {
this.#element = <HTMLElement>document.getElementById(ID);
this.#options = <HTMLElement>this.#element.querySelector(".ol-select-options");
this.#value = <HTMLElement>this.#element.querySelector(".ol-select-value");
this.#defaultValue = this.#value.innerText;
this.#callback = callback;
this.#callback = callback;
if (options != null) {
this.setOptions(options);
}
}
this.#value.addEventListener("click", (ev) => {
this.#element.classList.toggle("is-open");
@@ -31,11 +30,10 @@ export class Dropdown {
}
});
this.#options.classList.add( "ol-scrollable" );
this.#options.classList.add("ol-scrollable");
}
setOptions(optionsList: string[])
{
setOptions(optionsList: string[]) {
this.#optionsList = optionsList;
this.#options.replaceChildren(...optionsList.map((option: string, idx: number) => {
var div = document.createElement("div");
@@ -48,7 +46,9 @@ export class Dropdown {
button.addEventListener("click", (e: MouseEvent) => {
e.stopPropagation();
this.#value.innerHTML = `<div class = "ol-ellipsed"> ${option} </div>`;
this.#value = document.createElement("div");
this.#value.classList.add("ol-ellipsed");
this.#value.innerText = option;
this.#close();
this.#callback(option, e);
this.#index = idx;
@@ -57,21 +57,20 @@ export class Dropdown {
}));
}
selectText( text:string ) {
const index = [].slice.call( this.#options.children ).findIndex( ( opt:Element ) => opt.querySelector( "button" )?.innerText === text );
if ( index > -1 ) {
this.selectValue( index );
selectText(text: string) {
const index = [].slice.call(this.#options.children).findIndex((opt: Element) => opt.querySelector("button")?.innerText === text);
if (index > -1) {
this.selectValue(index);
}
}
selectValue(idx: number)
{
if (idx < this.#optionsList.length)
{
selectValue(idx: number) {
if (idx < this.#optionsList.length) {
var option = this.#optionsList[idx];
this.#value.innerHTML = `<div class = "ol-ellipsed"> ${option} </div>`;
var el = document.createElement("div");
el.classList.add("ol-ellipsed");
el.innerText = option;
this.#value.appendChild(el);
this.#index = idx;
this.#close();
this.#callback(option);
@@ -91,8 +90,8 @@ export class Dropdown {
}
setValue(value: string) {
var index = this.#optionsList.findIndex((option) => {return option === value});
if (index > -1)
var index = this.#optionsList.findIndex((option) => { return option === value });
if (index > -1)
this.selectValue(index);
}
@@ -102,21 +101,21 @@ export class Dropdown {
#clip() {
const options = this.#options;
const bounds = options.getBoundingClientRect();
this.#element.dataset.position = ( bounds.bottom > window.innerHeight ) ? "top" : "";
const bounds = options.getBoundingClientRect();
this.#element.dataset.position = (bounds.bottom > window.innerHeight) ? "top" : "";
}
#close() {
this.#element.classList.remove( "is-open" );
this.#element.classList.remove("is-open");
this.#element.dataset.position = "";
}
#open() {
this.#element.classList.add( "is-open" );
this.#element.classList.add("is-open");
}
#toggle() {
if ( this.#element.classList.contains( "is-open" ) ) {
if (this.#element.classList.contains("is-open")) {
this.#close();
} else {
this.#open();

View File

@@ -41,8 +41,7 @@ export class MapContextMenu extends ContextMenu {
document.addEventListener("contextMenuDeployAircraft", () => {
this.hide();
this.#spawnOptions.coalition = getActiveCoalition();
if (this.#spawnOptions)
{
if (this.#spawnOptions) {
getMap().addTemporaryMarker(this.#spawnOptions.latlng);
spawnAircraft(this.#spawnOptions);
}
@@ -51,8 +50,7 @@ export class MapContextMenu extends ContextMenu {
document.addEventListener("contextMenuDeployGroundUnit", () => {
this.hide();
this.#spawnOptions.coalition = getActiveCoalition();
if (this.#spawnOptions)
{
if (this.#spawnOptions) {
getMap().addTemporaryMarker(this.#spawnOptions.latlng);
spawnGroundUnit(this.#spawnOptions);
}
@@ -189,10 +187,10 @@ export class MapContextMenu extends ContextMenu {
this.#spawnOptions.role = role;
this.#resetGroundUnitType();
const types = groundUnitsDatabase.getByRole(role).map((blueprint) => { return blueprint.label } );
const types = groundUnitsDatabase.getByRole(role).map((blueprint) => { return blueprint.label });
types.sort();
this.#groundUnitTypeDropdown.setOptions( types );
this.#groundUnitTypeDropdown.setOptions(types);
this.#groundUnitTypeDropdown.selectValue(0);
this.clip();
}
@@ -205,8 +203,8 @@ export class MapContextMenu extends ContextMenu {
const roles = groundUnitsDatabase.getRoles();
roles.sort();
this.#groundUnitRoleDropdown.setOptions( roles );
this.#groundUnitRoleDropdown.setOptions(roles);
this.clip();
}

View File

@@ -23,8 +23,7 @@ export class Slider {
if (this.#container != null) {
this.#display = this.#container.style.display;
this.#slider = <HTMLInputElement>this.#container.querySelector("input");
if (this.#slider != null)
{
if (this.#slider != null) {
this.#slider.addEventListener("input", (e: any) => this.#onInput());
this.#slider.addEventListener("mousedown", (e: any) => this.#onStart());
this.#slider.addEventListener("mouseup", (e: any) => this.#onFinalize());
@@ -33,93 +32,77 @@ export class Slider {
}
}
show()
{
show() {
if (this.#container != null)
this.#container.style.display = this.#display;
}
hide()
{
hide() {
if (this.#container != null)
this.#container.style.display = 'none';
}
setActive(newActive: boolean)
{
if (this.#container && !this.#dragged)
{
setActive(newActive: boolean) {
if (this.#container && !this.#dragged) {
this.#container.classList.toggle("active", newActive);
if (!newActive && this.#valueText != null)
this.#valueText.innerText = "Mixed values";
}
}
setMinMax(newMinValue: number, newMaxValue: number)
{
setMinMax(newMinValue: number, newMaxValue: number) {
this.#minValue = newMinValue;
this.#maxValue = newMaxValue;
this.#updateMax();
}
setIncrement(newIncrement: number)
{
setIncrement(newIncrement: number) {
this.#increment = newIncrement;
this.#updateMax();
}
setValue(newValue: number)
{
setValue(newValue: number) {
// Disable value setting if the user is dragging the element
if (!this.#dragged)
{
if (!this.#dragged) {
this.#value = newValue;
if (this.#slider != null)
this.#slider.value = String((newValue - this.#minValue) / (this.#maxValue - this.#minValue) * parseFloat(this.#slider.max));
this.#slider.value = String((newValue - this.#minValue) / (this.#maxValue - this.#minValue) * parseFloat(this.#slider.max));
this.#onValue()
}
}
getValue()
{
getValue() {
return this.#value;
}
getDragged()
{
getDragged() {
return this.#dragged;
}
#updateMax()
{
#updateMax() {
var oldValue = this.getValue();
if (this.#slider != null)
this.#slider.max = String((this.#maxValue - this.#minValue) / this.#increment);
this.setValue(oldValue);
}
#onValue()
{
#onValue() {
if (this.#valueText != null && this.#slider != null)
this.#valueText.innerHTML = this.#minValue + Math.round(parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)) + this.#unit
this.#valueText.innerText = this.#minValue + Math.round(parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)) + this.#unit
this.setActive(true);
}
#onInput()
{
#onInput() {
this.#onValue();
}
#onStart()
{
#onStart() {
this.#dragged = true;
}
#onFinalize()
{
#onFinalize() {
this.#dragged = false;
if (this.#slider != null)
{
if (this.#slider != null) {
this.#value = this.#minValue + parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue);
this.#callback(this.getValue());
}

View File

@@ -1,4 +1,3 @@
import { getUnitsManager } from "..";
import { deg2rad } from "../other/utils";
import { ContextMenu } from "./contextmenu";
@@ -10,21 +9,19 @@ export class UnitContextMenu extends ContextMenu {
document.addEventListener("applyCustomFormation", () => {
var dialog = document.getElementById("custom-formation-dialog");
if (dialog)
{
if (dialog) {
dialog.classList.add("hide");
var clock = 1;
while (clock < 8)
{
if ((<HTMLInputElement> dialog.querySelector(`#formation-${clock}`)).checked)
while (clock < 8) {
if ((<HTMLInputElement>dialog.querySelector(`#formation-${clock}`)).checked)
break
clock++;
}
var angleDeg = 360 - (clock - 1) * 45;
var angleRad = deg2rad(angleDeg);
var distance = parseInt((<HTMLInputElement> dialog.querySelector(`#distance`)?.querySelector("input")).value) * 0.3048;
var upDown = parseInt((<HTMLInputElement> dialog.querySelector(`#up-down`)?.querySelector("input")).value) * 0.3048;
var distance = parseInt((<HTMLInputElement>dialog.querySelector(`#distance`)?.querySelector("input")).value) * 0.3048;
var upDown = parseInt((<HTMLInputElement>dialog.querySelector(`#up-down`)?.querySelector("input")).value) * 0.3048;
// X: front-rear, positive front
// Y: top-bottom, positive top
// Z: left-right, positive right
@@ -34,7 +31,7 @@ export class UnitContextMenu extends ContextMenu {
var z = distance * Math.sin(angleRad);
if (this.#customFormationCallback)
this.#customFormationCallback({"x": x, "y": y, "z": z})
this.#customFormationCallback({ "x": x, "y": y, "z": z })
}
})
}
@@ -43,13 +40,16 @@ export class UnitContextMenu extends ContextMenu {
this.#customFormationCallback = callback;
}
setOptions(options: {[key: string]: string}, callback: CallableFunction)
{
this.getContainer()?.replaceChildren(...Object.keys(options).map((option: string, idx: number) =>
{
setOptions(options: { [key: string]: {text: string, tooltip: string }}, callback: CallableFunction) {
this.getContainer()?.replaceChildren(...Object.keys(options).map((key: string, idx: number) => {
const option = options[key];
var button = document.createElement("button");
button.innerHTML = options[option];
button.addEventListener("click", () => callback(option));
var el = document.createElement("div");
el.title = option.tooltip;
el.innerText = option.text;
el.id = key;
button.addEventListener("click", () => callback(key));
button.appendChild(el);
return (button);
}));
}

View File

@@ -23,13 +23,13 @@ class FeatureSwitch {
userPreference;
constructor( config:FeatureSwitchInterface ) {
constructor(config: FeatureSwitchInterface) {
this.defaultEnabled = config.defaultEnabled;
this.label = config.label;
this.masterSwitch = config.masterSwitch;
this.name = config.name;
this.onEnabled = config.onEnabled;
this.label = config.label;
this.masterSwitch = config.masterSwitch;
this.name = config.name;
this.onEnabled = config.onEnabled;
this.userPreference = this.getUserPreference();
@@ -38,16 +38,16 @@ class FeatureSwitch {
getUserPreference() {
let preferences = JSON.parse( localStorage.getItem( "featureSwitches" ) || "{}" );
let preferences = JSON.parse(localStorage.getItem("featureSwitches") || "{}");
return ( preferences.hasOwnProperty( this.name ) ) ? preferences[ this.name ] : this.defaultEnabled;
return (preferences.hasOwnProperty(this.name)) ? preferences[this.name] : this.defaultEnabled;
}
isEnabled() {
if ( !this.masterSwitch ) {
if (!this.masterSwitch) {
return false;
}
@@ -58,7 +58,7 @@ class FeatureSwitch {
export class FeatureSwitches {
#featureSwitches:FeatureSwitch[] = [
#featureSwitches: FeatureSwitch[] = [
new FeatureSwitch({
"defaultEnabled": false,
@@ -66,7 +66,7 @@ export class FeatureSwitches {
"masterSwitch": true,
"name": "aic"
}),
new FeatureSwitch({
"defaultEnabled": false,
"label": "AI Formations",
@@ -74,21 +74,21 @@ export class FeatureSwitches {
"name": "ai-formations",
"removeArtifactsIfDisabled": false
}),
new FeatureSwitch({
"defaultEnabled": false,
"label": "ATC",
"masterSwitch": true,
"name": "atc"
}),
new FeatureSwitch({
"defaultEnabled": false,
"label": "Force show unit control panel",
"masterSwitch": true,
"name": "forceShowUnitControlPanel"
}),
new FeatureSwitch({
"defaultEnabled": true,
"label": "Show splash screen",
@@ -108,41 +108,41 @@ export class FeatureSwitches {
}
getSwitch( switchName:string ) {
getSwitch(switchName: string) {
return this.#featureSwitches.find( featureSwitch => featureSwitch.name === switchName );
return this.#featureSwitches.find(featureSwitch => featureSwitch.name === switchName);
}
#testSwitches() {
for ( const featureSwitch of this.#featureSwitches ) {
if ( featureSwitch.isEnabled() ) {
if ( typeof featureSwitch.onEnabled === "function" ) {
for (const featureSwitch of this.#featureSwitches) {
if (featureSwitch.isEnabled()) {
if (typeof featureSwitch.onEnabled === "function") {
featureSwitch.onEnabled();
}
} else {
document.querySelectorAll( "[data-feature-switch='" + featureSwitch.name + "']" ).forEach( el => {
if ( featureSwitch.removeArtifactsIfDisabled === false ) {
document.querySelectorAll("[data-feature-switch='" + featureSwitch.name + "']").forEach(el => {
if (featureSwitch.removeArtifactsIfDisabled === false) {
el.remove();
} else {
el.classList.add( "hide" );
el.classList.add("hide");
}
});
}
document.body.classList.toggle( "feature-" + featureSwitch.name, featureSwitch.isEnabled() );
document.body.classList.toggle("feature-" + featureSwitch.name, featureSwitch.isEnabled());
}
}
savePreferences() {
let preferences:any = {};
let preferences: any = {};
for ( const featureSwitch of this.#featureSwitches ) {
preferences[ featureSwitch.name ] = featureSwitch.isEnabled();
for (const featureSwitch of this.#featureSwitches) {
preferences[featureSwitch.name] = featureSwitch.isEnabled();
}
localStorage.setItem( "featureSwitches", JSON.stringify( preferences ) );
localStorage.setItem("featureSwitches", JSON.stringify(preferences));
}

View File

@@ -1,9 +1,9 @@
export abstract class ToggleableFeature {
#status:boolean = false;
#status: boolean = false;
constructor( defaultStatus:boolean ) {
constructor(defaultStatus: boolean) {
this.#status = defaultStatus;
@@ -12,17 +12,17 @@ export abstract class ToggleableFeature {
}
getStatus() : boolean {
getStatus(): boolean {
return this.#status;
}
protected onStatusUpdate() {}
protected onStatusUpdate() { }
toggleStatus( force?:boolean ) : void {
toggleStatus(force?: boolean): void {
if ( force ) {
if (force) {
this.#status = force;
} else {
this.#status = !this.#status;

View File

@@ -7,10 +7,10 @@ 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 { FeatureSwitches } from "./features/featureswitches";
import { LogPanel } from "./panels/logpanel";
import { getConfig, getPaused, setAddress, setCredentials, setPaused, startUpdate, toggleDemoEnabled } from "./server/server";
import { UnitDataTable } from "./units/unitdatatable";
import { UnitDataTable } from "./atc/unitdatatable";
import { keyEventWasInInput } from "./other/utils";
import { Popup } from "./popups/popup";
import { Dropdown } from "./controls/dropdown";
@@ -140,28 +140,14 @@ function setupEvents() {
case "Space":
setPaused(!getPaused());
break;
case "KeyW":
case "KeyA":
case "KeyS":
case "KeyD":
case "ArrowLeft":
case "ArrowRight":
case "ArrowUp":
case "ArrowDown":
case "KeyW": case "KeyA": case "KeyS": case "KeyD":
case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "ArrowDown":
getMap().handleMapPanning(ev);
break;
case "Digit1":
case "Digit2":
case "Digit3":
case "Digit4":
case "Digit5":
case "Digit6":
case "Digit7":
case "Digit8":
case "Digit9":
case "Digit1": case "Digit2": case "Digit3": case "Digit4": case "Digit5": case "Digit6": case "Digit7": case "Digit8": case "Digit9":
// Using the substring because the key will be invalid when pressing the Shift key
if (ev.ctrlKey && ev.shiftKey)
getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5)));
getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5)));
else if (ev.ctrlKey && !ev.shiftKey)
getUnitsManager().selectedUnitsSetHotgroup(parseInt(ev.code.substring(5)));
else
@@ -176,14 +162,7 @@ function setupEvents() {
return;
}
switch (ev.code) {
case "KeyW":
case "KeyA":
case "KeyS":
case "KeyD":
case "ArrowLeft":
case "ArrowRight":
case "ArrowUp":
case "ArrowDown":
case "KeyW": case "KeyA": case "KeyS": case "KeyD": case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "ArrowDown":
getMap().handleMapPanning(ev);
break;
}
@@ -201,8 +180,8 @@ function setupEvents() {
document.addEventListener("tryConnection", () => {
const form = document.querySelector("#splash-content")?.querySelector("#authentication-form");
const username = (<HTMLInputElement> (form?.querySelector("#username"))).value;
const password = (<HTMLInputElement> (form?.querySelector("#password"))).value;
const username = (<HTMLInputElement>(form?.querySelector("#username"))).value;
const password = (<HTMLInputElement>(form?.querySelector("#password"))).value;
setCredentials(username, btoa("admin" + ":" + password));
/* Start periodically requesting updates */
@@ -214,6 +193,7 @@ function setupEvents() {
document.addEventListener("reloadPage", () => {
location.reload();
})
}
export function getMap() {

View File

@@ -1,10 +1,10 @@
import { Map } from 'leaflet';
import { Handler} from 'leaflet';
import { Handler } from 'leaflet';
import { Util } from 'leaflet';
import { DomUtil } from 'leaflet';
import { DomEvent } from 'leaflet';
import { LatLngBounds } from 'leaflet';
import { Bounds } from 'leaflet';
import { Bounds } from 'leaflet';
export var BoxSelect = Handler.extend({
initialize: function (map: Map) {
@@ -82,12 +82,12 @@ export var BoxSelect = Handler.extend({
this._point = this._map.mouseEventToContainerPoint(e);
var bounds = new Bounds(this._point, this._startPoint),
size = bounds.getSize();
size = bounds.getSize();
if (bounds.min != undefined)
DomUtil.setPosition(this._box, bounds.min);
this._box.style.width = size.x + 'px';
this._box.style.width = size.x + 'px';
this._box.style.height = size.y + 'px';
},
@@ -113,7 +113,7 @@ export var BoxSelect = Handler.extend({
if ((e.which !== 1) && (e.button !== 0)) { return; }
this._finish();
if (!this._moved) { return; }
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
@@ -121,8 +121,8 @@ export var BoxSelect = Handler.extend({
var bounds = new LatLngBounds(
this._map.containerPointToLatLng(this._startPoint),
this._map.containerPointToLatLng(this._point));
this._map.fire('selectionend', {selectionBounds: bounds});
this._map.fire('selectionend', { selectionBounds: bounds });
},
_onKeyDown: function (e: any) {

View File

@@ -0,0 +1,12 @@
import { MiniMap, MiniMapOptions } from "leaflet-control-mini-map";
export class ClickableMiniMap extends MiniMap {
constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) {
super(layer, options);
}
getMap() {
//@ts-ignore needed to access not exported member. A bit of a hack, required to access click events //TODO: fix me
return this._miniMap;
}
}

View File

@@ -0,0 +1,25 @@
import { DivIcon, Map, Marker } from "leaflet";
import { MarkerOptions } from "leaflet";
import { LatLngExpression } from "leaflet";
export class CustomMarker extends Marker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
}
onAdd(map: Map): this {
this.setIcon(new DivIcon()); // Default empty icon
super.onAdd(map);
this.createIcon();
return this;
}
onRemove(map: Map): this {
super.onRemove(map);
return this;
}
createIcon() {
/* Overloaded by child classes */
}
}

View File

@@ -0,0 +1,15 @@
import { DivIcon } from "leaflet";
import { CustomMarker } from "./custommarker";
export class DestinationPreviewMarker extends CustomMarker {
createIcon() {
this.setIcon(new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 26],
className: "leaflet-destination-preview"
}));
var el = document.createElement("div");
el.classList.add("ol-destination-preview-icon");
this.getElement()?.appendChild(el);
}
}

View File

@@ -1,6 +1,4 @@
import * as L from "leaflet"
import { MiniMap, MiniMapOptions } from "leaflet-control-mini-map";
import { getUnitsManager } from "..";
import { BoxSelect } from "./boxselect";
import { MapContextMenu, SpawnOptions } from "../controls/mapcontextmenu";
@@ -9,31 +7,22 @@ import { AirbaseContextMenu } from "../controls/airbasecontextmenu";
import { Dropdown } from "../controls/dropdown";
import { Airbase } from "../missionhandler/airbase";
import { Unit } from "../units/unit";
// TODO a bit of a hack, this module is provided as pure javascript only
require("../../node_modules/leaflet.nauticscale/dist/leaflet.nauticscale.js")
export const IDLE = "IDLE";
export const MOVE_UNIT = "MOVE_UNIT";
import { bearing } from "../other/utils";
import { DestinationPreviewMarker } from "./destinationpreviewmarker";
import { TemporaryUnitMarker } from "./temporaryunitmarker";
import { ClickableMiniMap } from "./clickableminimap";
import { SVGInjector } from '@tanem/svg-injector'
L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect);
var temporaryIcon = new L.Icon({
iconUrl: 'images/icon-temporary.png',
iconSize: [52, 52],
iconAnchor: [26, 26]
});
// TODO would be nice to convert to ts
require("../../public/javascripts/leaflet.nauticscale.js")
export class ClickableMiniMap extends MiniMap {
constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) {
super(layer, options);
}
getMap() {
//@ts-ignore needed to access not exported member. A bit of a hack, required to access click events
return this._miniMap;
}
}
/* Map constants */
export const IDLE = "IDLE";
export const MOVE_UNIT = "MOVE_UNIT";
export const visibilityControls: string[] = ["human", "dcs", "aircraft", "groundunit-sam", "groundunit-other", "navyunit", "airbase"];
export const visibilityControlsTootlips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"];
export class Map extends L.Map {
#state: string;
@@ -51,12 +40,17 @@ export class Map extends L.Map {
#miniMap: ClickableMiniMap | null = null;
#miniMapLayerGroup: L.LayerGroup;
#temporaryMarkers: L.Marker[] = [];
#destinationPreviewMarkers: L.Marker[] = [];
#destinationGroupRotation: number = 0;
#computeDestinationRotation: boolean = false;
#destinationRotationCenter: L.LatLng | null = null;
#mapContextMenu: MapContextMenu = new MapContextMenu("map-contextmenu");
#unitContextMenu: UnitContextMenu = new UnitContextMenu("unit-contextmenu");
#airbaseContextMenu: AirbaseContextMenu = new AirbaseContextMenu("airbase-contextmenu");
#mapSourceDropdown: Dropdown;
#optionButtons: { [key: string]: HTMLButtonElement[] } = {}
constructor(ID: string) {
/* Init the leaflet map */
@@ -67,53 +61,18 @@ export class Map extends L.Map {
this.setLayer("ArcGIS Satellite");
/* Minimap */
/* Draw the limits of the maps in the minimap*/
var latlngs = [[ // NTTR
new L.LatLng(39.7982463, -119.985425),
new L.LatLng(34.4037128, -119.7806729),
new L.LatLng(34.3483316, -112.4529351),
new L.LatLng(39.7372411, -112.1130805),
new L.LatLng(39.7982463, -119.985425)
],
[ // Syria
new L.LatLng(37.3630556, 29.2686111),
new L.LatLng(31.8472222, 29.8975),
new L.LatLng(32.1358333, 42.1502778),
new L.LatLng(37.7177778, 42.3716667),
new L.LatLng(37.3630556, 29.2686111)
],
[ // Caucasus
new L.LatLng(39.6170191, 27.634935),
new L.LatLng(38.8735863, 47.1423108),
new L.LatLng(47.3907982, 49.3101946),
new L.LatLng(48.3955879, 26.7753625),
new L.LatLng(39.6170191, 27.634935)
],
[ // Persian Gulf
new L.LatLng(32.9355285, 46.5623682),
new L.LatLng(21.729393, 47.572675),
new L.LatLng(21.8501348, 63.9734737),
new L.LatLng(33.131584, 64.7313594),
new L.LatLng(32.9355285, 46.5623682)
],
[ // Marianas
new L.LatLng(22.09, 135.0572222),
new L.LatLng(10.5777778, 135.7477778),
new L.LatLng(10.7725, 149.3918333),
new L.LatLng(22.5127778, 149.5427778),
new L.LatLng(22.09, 135.0572222)
]
];
var minimapLayer = new L.TileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { minZoom: 0, maxZoom: 13 });
this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]);
var miniMapPolyline = new L.Polyline(latlngs, { color: '#202831' });
var miniMapPolyline = new L.Polyline(this.#getMinimapBoundaries(), { color: '#202831' });
miniMapPolyline.addTo(this.#miniMapLayerGroup);
/* Scale */
//@ts-ignore TODO more hacking because the module is provided as a pure javascript module only
L.control.scalenautic({ position: "topright", maxWidth: 300, nautic: true, metric: true, imperial: false }).addTo(this);
/* Map source dropdown */
this.#mapSourceDropdown = new Dropdown("map-type", (layerName: string) => this.setLayer(layerName), this.getLayers())
/* Init the state machine */
this.#state = IDLE;
@@ -127,15 +86,21 @@ export class Map extends L.Map {
this.on('mousedown', (e: any) => this.#onMouseDown(e));
this.on('mouseup', (e: any) => this.#onMouseUp(e));
this.on('mousemove', (e: any) => this.#onMouseMove(e));
this.on('keydown', (e: any) => this.#updateDestinationPreview(e));
this.on('keyup', (e: any) => this.#updateDestinationPreview(e));
/* Event listeners */
document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => {
ev.detail._element.classList.toggle("off");
document.body.toggleAttribute("data-hide-" + ev.detail.coalition);
const el = ev.detail._element;
el?.classList.toggle("off");
getUnitsManager().setHiddenType(ev.detail.coalition, (el?.currentTarget as HTMLElement)?.classList.contains("off"));
Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility());
});
document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => {
document.body.toggleAttribute("data-hide-" + ev.detail.category);
const el = ev.detail._element;
el?.classList.toggle("off");
getUnitsManager().setHiddenType(ev.detail.type, !el?.classList.contains("off"));
Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility());
});
@@ -144,12 +109,17 @@ export class Map extends L.Map {
this.#panToUnit(this.#centerUnit);
});
this.#mapSourceDropdown = new Dropdown("map-type", (layerName: string) => this.setLayer(layerName), this.getLayers())
this.#panInterval = window.setInterval(() => {
this.panBy(new L.Point( ((this.#panLeft? -1: 0) + (this.#panRight? 1: 0)) * this.#deafultPanDelta,
((this.#panUp? -1: 0) + (this.#panDown? 1: 0)) * this.#deafultPanDelta));
/* Pan interval */
this.#panInterval = window.setInterval(() => {
this.panBy(new L.Point(((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.#deafultPanDelta,
((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.#deafultPanDelta));
}, 20);
/* Option buttons */
this.#optionButtons["visibility"] = visibilityControls.map((option: string, index: number) => {
return this.#createOptionButton(option, `visibility/${option.toLowerCase()}.svg`, visibilityControlsTootlips[index], "toggleUnitVisibility", `{"type": "${option}"}`);
});
document.querySelector("#unit-visibility-control")?.append(...this.#optionButtons["visibility"]);
}
setLayer(layerName: string) {
@@ -205,9 +175,34 @@ export class Map extends L.Map {
this.#state = state;
if (this.#state === IDLE) {
L.DomUtil.removeClass(this.getContainer(), 'crosshair-cursor-enabled');
/* Remove all the destination preview markers */
this.#destinationPreviewMarkers.forEach((marker: L.Marker) => {
this.removeLayer(marker);
})
this.#destinationPreviewMarkers = [];
this.#destinationGroupRotation = 0;
this.#computeDestinationRotation = false;
this.#destinationRotationCenter = null;
}
else if (this.#state === MOVE_UNIT) {
L.DomUtil.addClass(this.getContainer(), 'crosshair-cursor-enabled');
/* Remove all the exising destination preview markers */
this.#destinationPreviewMarkers.forEach((marker: L.Marker) => {
this.removeLayer(marker);
})
this.#destinationPreviewMarkers = [];
if (getUnitsManager().getSelectedUnits({ excludeHumans: true }).length > 1 && getUnitsManager().getSelectedUnits({ excludeHumans: true }).length < 20) {
/* Create the unit destination preview markers */
this.#destinationPreviewMarkers = getUnitsManager().getSelectedUnits({ excludeHumans: true }).map((unit: Unit) => {
var marker = new DestinationPreviewMarker(this.getMouseCoordinates());
marker.addTo(this);
return marker;
})
}
}
document.dispatchEvent(new CustomEvent("mapStateChanged"));
}
@@ -328,7 +323,6 @@ export class Map extends L.Map {
if (this.#miniMap)
this.setView(e.latlng);
})
}
getMiniMapLayerGroup() {
@@ -336,7 +330,7 @@ export class Map extends L.Map {
}
handleMapPanning(e: any) {
if (e.type === "keyup"){
if (e.type === "keyup") {
switch (e.code) {
case "KeyA":
case "ArrowLeft":
@@ -356,9 +350,8 @@ export class Map extends L.Map {
break;
}
}
else {
switch (e.code)
{
else {
switch (e.code) {
case 'KeyA':
case 'ArrowLeft':
this.#panLeft = true;
@@ -380,7 +373,7 @@ export class Map extends L.Map {
}
addTemporaryMarker(latlng: L.LatLng) {
var marker = new L.Marker(latlng, {icon: temporaryIcon});
var marker = new TemporaryUnitMarker(latlng);
marker.addTo(this);
this.#temporaryMarkers.push(marker);
}
@@ -397,8 +390,7 @@ export class Map extends L.Map {
i = idx;
}
});
if (closest)
{
if (closest) {
this.removeLayer(closest);
delete this.#temporaryMarkers[i];
}
@@ -433,7 +425,10 @@ export class Map extends L.Map {
if (!e.originalEvent.ctrlKey) {
getUnitsManager().selectedUnitsClearDestinations();
}
getUnitsManager().selectedUnitsAddDestination(e.latlng)
getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, !e.originalEvent.shiftKey, this.#destinationGroupRotation)
this.#destinationGroupRotation = 0;
this.#destinationRotationCenter = null;
this.#computeDestinationRotation = false;
}
}
@@ -448,6 +443,16 @@ export class Map extends L.Map {
#onMouseDown(e: any) {
this.hideAllContextMenus();
if (this.#state == MOVE_UNIT) {
this.#destinationGroupRotation = 0;
this.#destinationRotationCenter = null;
this.#computeDestinationRotation = false;
if (e.originalEvent.button == 2) {
this.#computeDestinationRotation = true;
this.#destinationRotationCenter = this.getMouseCoordinates();
}
}
}
#onMouseUp(e: any) {
@@ -456,6 +461,11 @@ export class Map extends L.Map {
#onMouseMove(e: any) {
this.#lastMousePosition.x = e.originalEvent.x;
this.#lastMousePosition.y = e.originalEvent.y;
if (this.#computeDestinationRotation && this.#destinationRotationCenter != null)
this.#destinationGroupRotation = -bearing(this.#destinationRotationCenter.lat, this.#destinationRotationCenter.lng, this.getMouseCoordinates().lat, this.getMouseCoordinates().lng);
this.#updateDestinationPreview(e);
}
#onZoom(e: any) {
@@ -467,4 +477,65 @@ export class Map extends L.Map {
var unitPosition = new L.LatLng(unit.getFlightData().latitude, unit.getFlightData().longitude);
this.setView(unitPosition, this.getZoom(), { animate: false });
}
#getMinimapBoundaries() {
/* Draw the limits of the maps in the minimap*/
return [[ // NTTR
new L.LatLng(39.7982463, -119.985425),
new L.LatLng(34.4037128, -119.7806729),
new L.LatLng(34.3483316, -112.4529351),
new L.LatLng(39.7372411, -112.1130805),
new L.LatLng(39.7982463, -119.985425)
],
[ // Syria
new L.LatLng(37.3630556, 29.2686111),
new L.LatLng(31.8472222, 29.8975),
new L.LatLng(32.1358333, 42.1502778),
new L.LatLng(37.7177778, 42.3716667),
new L.LatLng(37.3630556, 29.2686111)
],
[ // Caucasus
new L.LatLng(39.6170191, 27.634935),
new L.LatLng(38.8735863, 47.1423108),
new L.LatLng(47.3907982, 49.3101946),
new L.LatLng(48.3955879, 26.7753625),
new L.LatLng(39.6170191, 27.634935)
],
[ // Persian Gulf
new L.LatLng(32.9355285, 46.5623682),
new L.LatLng(21.729393, 47.572675),
new L.LatLng(21.8501348, 63.9734737),
new L.LatLng(33.131584, 64.7313594),
new L.LatLng(32.9355285, 46.5623682)
],
[ // Marianas
new L.LatLng(22.09, 135.0572222),
new L.LatLng(10.5777778, 135.7477778),
new L.LatLng(10.7725, 149.3918333),
new L.LatLng(22.5127778, 149.5427778),
new L.LatLng(22.09, 135.0572222)
]
];
}
#updateDestinationPreview(e: any) {
Object.values(getUnitsManager().selectedUnitsComputeGroupDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : this.getMouseCoordinates(), this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => {
if (idx < this.#destinationPreviewMarkers.length)
this.#destinationPreviewMarkers[idx].setLatLng(!e.originalEvent.shiftKey ? latlng : this.getMouseCoordinates());
})
}
#createOptionButton(value: string, url: string, title: string, callback: string, argument: string) {
var button = document.createElement("button");
const img = document.createElement("img");
img.src = `/resources/theme/images/buttons/${url}`;
img.onload = () => SVGInjector(img);
button.title = title;
button.value = value;
button.appendChild(img);
button.setAttribute("data-on-click", callback);
button.setAttribute("data-on-click-params", argument);
return button;
}
}

View File

@@ -0,0 +1,13 @@
import { Icon } from "leaflet";
import { CustomMarker } from "./custommarker";
export class TemporaryUnitMarker extends CustomMarker {
createIcon() {
var icon = new Icon({
iconUrl: '/resources/theme/images/markers/temporary-icon.png',
iconSize: [52, 52],
iconAnchor: [26, 26]
});
this.setIcon(icon);
}
}

View File

@@ -1,13 +1,14 @@
import * as L from 'leaflet'
import { DivIcon } from 'leaflet';
import { CustomMarker } from '../map/custommarker';
import { SVGInjector } from '@tanem/svg-injector';
export interface AirbaseOptions
{
name: string,
position: L.LatLng,
src: string
position: L.LatLng
}
export class Airbase extends L.Marker
export class Airbase extends CustomMarker
{
#name: string = "";
#coalition: string = "";
@@ -19,21 +20,30 @@ export class Airbase extends L.Marker
super(options.position, { riseOnHover: true });
this.#name = options.name;
var icon = new L.DivIcon({
html: ` <div class="airbase" data-object="airbase" data-coalition="neutral">
<div class="airbase-marker"> </div>
</div>`,
}
createIcon() {
var icon = new DivIcon({
className: 'leaflet-airbase-marker',
iconSize: [40, 40],
iconAnchor: [20, 20]
}); // Set the marker, className must be set to avoid white square
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("airbase-icon");
el.setAttribute("data-object", "airbase");
var img = document.createElement("img");
img.src = "/resources/theme/images/markers/airbase.svg";
img.onload = () => SVGInjector(img);
el.appendChild(img);
this.getElement()?.appendChild(el);
}
setCoalition(coalition: string)
{
this.#coalition = coalition;
(<HTMLElement> this.getElement()?.querySelector(".airbase")).dataset.coalition = this.#coalition;
(<HTMLElement> this.getElement()?.querySelector(".airbase-icon")).dataset.coalition = this.#coalition;
}
getCoalition()

View File

@@ -0,0 +1,36 @@
import { DivIcon } from "leaflet";
import { CustomMarker } from "../map/custommarker";
import { SVGInjector } from "@tanem/svg-injector";
export class Bullseye extends CustomMarker {
#coalition: string = "";
createIcon() {
var icon = new DivIcon({
className: 'leaflet-bullseye-marker',
iconSize: [40, 40],
iconAnchor: [20, 20]
}); // Set the marker, className must be set to avoid white square
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("bullseye-icon");
el.setAttribute("data-object", "bullseye");
var img = document.createElement("img");
img.src = "/resources/theme/images/markers/bullseye.svg";
img.onload = () => SVGInjector(img);
el.appendChild(img);
this.getElement()?.appendChild(el);
}
setCoalition(coalition: string)
{
this.#coalition = coalition;
(<HTMLElement> this.getElement()?.querySelector(".bullseye-icon")).dataset.coalition = this.#coalition;
}
getCoalition()
{
return this.#coalition;
}
}

View File

@@ -1,44 +1,58 @@
import { Marker, LatLng, Icon } from "leaflet";
import { LatLng } from "leaflet";
import { getInfoPopup, getMap } from "..";
import { Airbase } from "./airbase";
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]})
]
import { Bullseye } from "./bullseye";
export class MissionHandler
{
#bullseyes : any; //TODO declare interface
#bullseyeMarkers: any;
#airbases : any; //TODO declare interface
#airbasesMarkers: {[name: string]: Airbase};
#bullseyes : {[name: string]: Bullseye} = {};
#airbases : {[name: string]: Airbase} = {};
#theatre : string = "";
constructor()
{
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: BullseyesData | AirbasesData | any)
{
if ("bullseyes" in data)
{
this.#bullseyes = data.bullseyes;
this.#drawBullseyes();
for (let idx in data.bullseyes)
{
const bullseye = data.bullseyes[idx];
if (!(idx in this.#bullseyes))
this.#bullseyes[idx] = new Bullseye([0, 0]).addTo(getMap());
if (bullseye.latitude && bullseye.longitude && bullseye.coalition)
{
this.#bullseyes[idx].setLatLng(new LatLng(bullseye.latitude, bullseye.longitude));
this.#bullseyes[idx].setCoalition(bullseye.coalition);
}
}
}
if ("airbases" in data)
{
this.#airbases = data.airbases;
this.#drawAirbases();
for (let idx in data.airbases)
{
var airbase = data.airbases[idx]
if (this.#airbases[idx] === undefined)
{
this.#airbases[idx] = new Airbase({
position: new LatLng(airbase.latitude, airbase.longitude),
name: airbase.callsign
}).addTo(getMap());
this.#airbases[idx].on('contextmenu', (e) => this.#onAirbaseClick(e));
}
if (airbase.latitude && airbase.longitude && airbase.coalition)
{
this.#airbases[idx].setLatLng(new LatLng(airbase.latitude, airbase.longitude));
this.#airbases[idx].setCoalition(airbase.coalition);
}
//this.#airbases[idx].setProperties(["Runway 1: 31L / 13R", "Runway 2: 31R / 13L", "TCN: 17X", "ILS: ---" ]);
//this.#airbases[idx].setParkings(["2x big", "5x small"]);
}
}
if ("mission" in data)
@@ -58,38 +72,6 @@ export class MissionHandler
return this.#bullseyes;
}
#drawBullseyes()
{
for (let idx in this.#bullseyes)
{
var bullseye = this.#bullseyes[idx];
this.#bullseyeMarkers[idx].setLatLng(new LatLng(bullseye.latitude, bullseye.longitude));
}
}
#drawAirbases()
{
for (let idx in this.#airbases)
{
var airbase = this.#airbases[idx]
if (this.#airbasesMarkers[idx] === undefined)
{
this.#airbasesMarkers[idx] = new Airbase({
position: new LatLng(airbase.latitude, airbase.longitude),
name: airbase.callsign,
src: "images/airbase.png"}).addTo(getMap());
this.#airbasesMarkers[idx].on('contextmenu', (e) => this.#onAirbaseClick(e));
}
else
{
this.#airbasesMarkers[idx].setLatLng(new LatLng(airbase.latitude, airbase.longitude));
this.#airbasesMarkers[idx].setCoalition(airbase.coalition);
//this.#airbasesMarkers[idx].setProperties(["Runway 1: 31L / 13R", "Runway 2: 31R / 13L", "TCN: 17X", "ILS: ---" ]);
//this.#airbasesMarkers[idx].setParkings(["2x big", "5x small"]);
}
}
}
#onAirbaseClick(e: any)
{
getMap().showAirbaseContextMenu(e, e.sourceTarget);

View File

@@ -11,48 +11,6 @@ export function bearing(lat1: number, lon1: number, lat2: number, lon2: number)
return brng;
}
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 dataPointMap( container:HTMLElement, data:any) {
Object.keys( data ).forEach( ( key ) => {
const val = "" + data[ key ]; // Ensure a string
container.querySelectorAll( `[data-point="${key}"]`).forEach( el => {
// We could probably have options here
if ( el instanceof HTMLInputElement ) {
el.value = val;
} else if ( el instanceof HTMLElement ) {
el.innerText = val;
}
});
});
}
export function deg2rad(deg: number) {
var pi = Math.PI;
return deg * (pi / 180);
}
export function distance(lat1: number, lon1: number, lat2: number, lon2: number) {
const R = 6371e3; // metres
const φ1 = deg2rad(lat1); // φ, λ in radians
@@ -68,6 +26,42 @@ export function distance(lat1: number, lon1: number, lat2: number, lon2: number)
return d;
}
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 dataPointMap( container:HTMLElement, data:any) {
Object.keys( data ).forEach( ( key ) => {
const val = "" + data[ key ]; // Ensure a string
container.querySelectorAll( `[data-point="${key}"]`).forEach( el => {
// We could probably have options here
if ( el instanceof HTMLInputElement ) {
el.value = val;
} else if ( el instanceof HTMLElement ) {
el.innerText = val;
}
});
});
}
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);
}
export function generateUUIDv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
@@ -76,33 +70,15 @@ export function generateUUIDv4() {
});
}
export function keyEventWasInInput( event:KeyboardEvent ) {
const target = event.target;
return ( target instanceof HTMLElement && ( [ "INPUT", "TEXTAREA" ].includes( target.nodeName ) ) );
}
export function rad2deg(rad: number) {
var pi = Math.PI;
return rad / (pi / 180);
}
export function reciprocalHeading(heading: number): number {
if (heading > 180) {
return heading - 180;
}
return heading + 180;
return heading > 180? heading - 180: heading + 180;
}
export const zeroAppend = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
@@ -111,7 +87,6 @@ export const zeroAppend = function (num: number, places: number) {
return string;
}
export const zeroPad = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
@@ -120,7 +95,6 @@ export const zeroPad = function (num: number, places: number) {
return string;
}
export function similarity(s1: string, s2: string) {
var longer = s1;
var shorter = s2;
@@ -160,4 +134,30 @@ export function editDistance(s1: string, s2: string) {
costs[s2.length] = lastValue;
}
return costs[s2.length];
}
export function latLngToMercator(lat: number, lng: number): {x: number, y: number} {
var rMajor = 6378137; //Equatorial Radius, WGS84
var shift = Math.PI * rMajor;
var x = lng * shift / 180;
var y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
y = y * shift / 180;
return {x: x, y: y};
}
export function mercatorToLatLng(x: number, y: number) {
var rMajor = 6378137; //Equatorial Radius, WGS84
var shift = Math.PI * rMajor;
var lng = x / shift * 180.0;
var lat = y / shift * 180.0;
lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0);
return { lng: lng, lat: lat };
}
export function createDivWithClass(className: string) {
var el = document.createElement("div");
el.classList.add(className);
return el;
}

View File

@@ -18,14 +18,24 @@ export class HotgroupPanel extends Panel {
}
addHotgroup(hotgroup: number) {
const hotgroupHtml = `<div class="unit-hotgroup">
<div class="unit-hotgroup-id">${hotgroup}</div>
</div>
x${getUnitsManager().getUnitsByHotgroup(hotgroup).length}`
// Hotgroup number
var hotgroupDiv = document.createElement("div");
hotgroupDiv.classList.add("unit-hotgroup");
var idDiv = document.createElement("div");
idDiv.classList.add("unit-hotgroup-id");
idDiv.innerText = String(hotgroup);
hotgroupDiv.appendChild(idDiv);
// Hotgroup unit count
var countDiv = document.createElement("div");
countDiv.innerText = `x${getUnitsManager().getUnitsByHotgroup(hotgroup).length}`;
var el = document.createElement("div");
el.appendChild(hotgroupDiv);
el.appendChild(countDiv);
el.classList.add("hotgroup-selector");
el.innerHTML = hotgroupHtml;
el.toggleAttribute(`data-hotgroup-${hotgroup}`, true)
this.getElement().appendChild(el);
el.addEventListener("click", () => {

View File

@@ -37,8 +37,8 @@ export class MouseInfoPanel extends Panel {
if ( el != null ) {
var dist = distance(bullseyes[idx].latitude, bullseyes[idx].longitude, mousePosition.lat, mousePosition.lng);
var bear = bearing(bullseyes[idx].latitude, bullseyes[idx].longitude, mousePosition.lat, mousePosition.lng);
var dist = distance(bullseyes[idx].getLatLng().lat, bullseyes[idx].getLatLng().lng, mousePosition.lat, mousePosition.lng);
var bear = bearing(bullseyes[idx].getLatLng().lat, bullseyes[idx].getLatLng().lng, mousePosition.lat, mousePosition.lng);
let bng = zeroAppend(Math.floor(bear), 3);

View File

@@ -1,3 +1,4 @@
import { SVGInjector } from "@tanem/svg-injector";
import { getUnitsManager } from "..";
import { Dropdown } from "../controls/dropdown";
import { Slider } from "../controls/slider";
@@ -8,12 +9,12 @@ import { UnitDatabase } from "../units/unitdatabase";
import { Panel } from "./panel";
const ROEs: string[] = ["Hold", "Return", "Designated", "Free"];
const reactionsToThreat: string[] = ["None", "Passive", "Evade"];
const reactionsToThreat: string[] = ["None", "Manoeuvre", "Passive", "Evade"];
const emissionsCountermeasures: string[] = ["Silent", "Attack", "Defend", "Free"];
const ROEDescriptions: string[] = ["Hold (Never fire)", "Return (Only fire if fired upon)", "Designated (Attack the designated target only)", "Free (Attack anyone)"];
const reactionsToThreatDescriptions: string[] = ["None (No reaction)", "Passive (Countermeasures only, no manoeuvre)", "Evade (Countermeasures and manoeuvers)"];
const emissionsCountermeasuresDescriptions: string[] = ["Silent (Radar off, no countermeasures)", "Attack (Radar only for targeting, countermeasures only if attacked/locked)", "Defend (Radar for searching, jammer if locked, countermeasures inside WEZ)", "Always on (Radar and jammer always on, countermeasures when hostile detected)"];
const reactionsToThreatDescriptions: string[] = ["None (No reaction)", "Manoeuvre (no countermeasures)", "Passive (Countermeasures only, no manoeuvre)", "Evade (Countermeasures and manoeuvers)"];
const emissionsCountermeasuresDescriptions: string[] = ["Silent (Radar OFF, no ECM)", "Attack (Radar only for targeting, ECM only if locked)", "Defend (Radar for searching, ECM if locked)", "Always on (Radar and ECM always on)"];
const minSpeedValues: { [key: string]: number } = { Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0 };
const maxSpeedValues: { [key: string]: number } = { Aircraft: 800, Helicopter: 300, NavyUnit: 60, GroundUnit: 60 };
@@ -56,15 +57,15 @@ export class UnitControlPanel extends Panel {
/* Option buttons */
this.#optionButtons["ROE"] = ROEs.map((option: string, index: number) => {
return this.#createOptionButton(option, ROEDescriptions[index], () => { getUnitsManager().selectedUnitsSetROE(option); });
return this.#createOptionButton(option, `roe/${option.toLowerCase()}.svg`, ROEDescriptions[index], () => { getUnitsManager().selectedUnitsSetROE(option); });
});
this.#optionButtons["reactionToThreat"] = reactionsToThreat.map((option: string, index: number) => {
return this.#createOptionButton(option, reactionsToThreatDescriptions[index],() => { getUnitsManager().selectedUnitsSetReactionToThreat(option); });
return this.#createOptionButton(option, `threat/${option.toLowerCase()}.svg`, reactionsToThreatDescriptions[index],() => { getUnitsManager().selectedUnitsSetReactionToThreat(option); });
});
this.#optionButtons["emissionsCountermeasures"] = emissionsCountermeasures.map((option: string, index: number) => {
return this.#createOptionButton(option, emissionsCountermeasuresDescriptions[index],() => { getUnitsManager().selectedUnitsSetEmissionsCountermeasures(option); });
return this.#createOptionButton(option, `emissions/${option.toLowerCase()}.svg`, emissionsCountermeasuresDescriptions[index],() => { getUnitsManager().selectedUnitsSetEmissionsCountermeasures(option); });
});
this.getElement().querySelector("#roe-buttons-container")?.append(...this.#optionButtons["ROE"]);
@@ -342,10 +343,14 @@ export class UnitControlPanel extends Panel {
this.#advancedSettingsDialog.classList.add("hide");
}
#createOptionButton(option: string, title: string, callback: EventListenerOrEventListenerObject) {
#createOptionButton(value: string, url: string, title: string, callback: EventListenerOrEventListenerObject) {
var button = document.createElement("button");
button.value = option;
button.title = title;
button.value = value;
var img = document.createElement("img");
img.src = `/resources/theme/images/buttons/${url}`;
img.onload = () => SVGInjector(img);
button.appendChild(img);
button.addEventListener("click", callback);
return button;
}

View File

@@ -322,7 +322,7 @@ export class AircraftDatabase extends UnitDatabase {
},
"H-6J": {
"name": "H-6J",
"label": "H-6J Badger,
"label": "H-6J Badger",
"era": ["Mid Cold War, Late Cold War", "Modern"],
"shortLabel": "H6",
"loadouts": [

View File

@@ -4,6 +4,24 @@ export class GroundUnitsDatabase extends UnitDatabase {
constructor() {
super();
this.blueprints = {
"SA-2 SAM Battery": {
"name": "SA-2 SAM Battery",
"label": "SA-2 SAM Battery",
"shortLabel": "SA-2 SAM Battery",
"loadouts": [
{
"fuel": 1,
"items": [
],
"roles": [
"Template"
],
"code": "",
"name": "Default"
}
],
"filename": ""
},
"2B11 mortar": {
"name": "2B11 mortar",
"label": "2B11 mortar",

View File

@@ -4,14 +4,17 @@ import { rad2deg } from '../other/utils';
import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures } from '../server/server';
import { aircraftDatabase } from './aircraftdatabase';
import { groundUnitsDatabase } from './groundunitsdatabase';
import { CustomMarker } from '../map/custommarker';
import { SVGInjector } from '@tanem/svg-injector';
import { UnitDatabase } from './unitdatabase';
var pathIcon = new Icon({
iconUrl: 'images/marker-icon.png',
shadowUrl: 'images/marker-shadow.png',
iconUrl: '/resources/theme/images/markers/marker-icon.png',
shadowUrl: '/resources/theme/images/markers/marker-shadow.png',
iconAnchor: [13, 41]
});
export class Unit extends Marker {
export class Unit extends CustomMarker {
ID: number;
#data: UnitData = {
@@ -114,22 +117,7 @@ export class Unit extends Marker {
/* Set the unit data */
this.setData(data);
/* Set the icon */
var icon = new DivIcon({
html: this.getMarkerHTML(),
className: 'leaflet-unit-marker',
iconAnchor: [25, 25],
iconSize: [50, 50],
});
this.setIcon(icon);
}
getMarkerHTML() {
return `<div class="unit" data-object="unit-${this.getMarkerCategory()}" data-coalition="${this.getMissionData().coalition}">
<div class="unit-selected-spotlight"></div>
<div class="unit-marker"></div>
<div class="unit-short-label"></div>
</div>`
}
getMarkerCategory() {
@@ -137,8 +125,77 @@ export class Unit extends Marker {
return "";
}
/********************** Unit data *************************/
getDatabase(): UnitDatabase | null {
// Overloaded by child classes
return null;
}
getIconOptions(): UnitIconOptions {
// Default values, overloaded by child classes if needed
return {
showState: false,
showVvi: false,
showHotgroup: false,
showUnitIcon: true,
showShortLabel: false,
showFuel: false,
showAmmo: false,
showSummary: false,
rotateToHeading: false
}
}
setSelected(selected: boolean) {
/* Only alive units can be selected. Some units are not selectable (weapons) */
if ((this.getBaseData().alive || !selected) && this.getSelectable() && this.getSelected() != selected) {
this.#selected = selected;
this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-selected", selected);
if (selected)
document.dispatchEvent(new CustomEvent("unitSelection", { detail: this }));
else
document.dispatchEvent(new CustomEvent("unitDeselection", { detail: this }));
this.getGroupMembers().forEach((unit: Unit) => unit.setSelected(selected));
}
}
getSelected() {
return this.#selected;
}
setSelectable(selectable: boolean) {
this.#selectable = selectable;
}
getSelectable() {
return this.#selectable;
}
setHotgroup(hotgroup: number | null) {
this.#hotgroup = hotgroup;
this.#updateMarker();
}
getHotgroup() {
return this.#hotgroup;
}
setHighlighted(highlighted: boolean) {
if (this.getSelectable() && this.#highlighted != highlighted) {
this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-highlighted", highlighted);
this.#highlighted = highlighted;
this.getGroupMembers().forEach((unit: Unit) => unit.setHighlighted(highlighted));
}
}
getHighlighted() {
return this.#highlighted;
}
getGroupMembers() {
return Object.values(getUnitsManager().getUnits()).filter((unit: Unit) => {return unit != this && unit.getBaseData().groupName === this.getBaseData().groupName;});
}
/********************** Unit data *************************/
setData(data: UpdateData) {
/* Check if data has changed comparing new values to old values */
const positionChanged = (data.flightData != undefined && data.flightData.latitude != undefined && data.flightData.longitude != undefined && (this.getFlightData().latitude != data.flightData.latitude || this.getFlightData().longitude != data.flightData.longitude));
@@ -237,52 +294,119 @@ export class Unit extends Marker {
return this.getData().optionsData;
}
setSelected(selected: boolean) {
/* Only alive units can be selected. Some units are not selectable (weapons) */
if ((this.getBaseData().alive || !selected) && this.getSelectable() && this.getSelected() != selected) {
this.#selected = selected;
this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-selected");
if (selected)
document.dispatchEvent(new CustomEvent("unitSelection", { detail: this }));
else
document.dispatchEvent(new CustomEvent("unitDeselection", { detail: this }));
/********************** Icon *************************/
createIcon(): void {
/* Set the icon */
var icon = new DivIcon({
className: 'leaflet-unit-icon',
iconAnchor: [25, 25],
iconSize: [50, 50],
});
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("unit");
el.setAttribute("data-object", `unit-${this.getMarkerCategory()}`);
el.setAttribute("data-coalition", this.getMissionData().coalition);
// Generate and append elements depending on active options
// Velocity vector
if (this.getIconOptions().showVvi) {
var vvi = document.createElement("div");
vvi.classList.add("unit-vvi");
vvi.toggleAttribute("data-rotate-to-heading");
el.append(vvi);
}
}
getSelected() {
return this.#selected;
}
// Hotgroup indicator
if (this.getIconOptions().showHotgroup) {
var hotgroup = document.createElement("div");
hotgroup.classList.add("unit-hotgroup");
var hotgroupId = document.createElement("div");
hotgroupId.classList.add("unit-hotgroup-id");
hotgroup.appendChild(hotgroupId);
el.append(hotgroup);
}
setSelectable(selectable: boolean) {
this.#selectable = selectable;
}
// Main icon
if (this.getIconOptions().showUnitIcon) {
var unitIcon = document.createElement("div");
unitIcon.classList.add("unit-icon");
var img = document.createElement("img");
img.src = `/resources/theme/images/units/${this.getMarkerCategory()}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
unitIcon.toggleAttribute("data-rotate-to-heading", this.getIconOptions().rotateToHeading);
el.append(unitIcon);
}
getSelectable() {
return this.#selectable;
}
// State icon
if (this.getIconOptions().showState){
var state = document.createElement("div");
state.classList.add("unit-state");
el.appendChild(state);
}
setHotgroup(hotgroup: number | null) {
this.#hotgroup = hotgroup;
}
// Short label
if (this.getIconOptions().showShortLabel) {
var shortLabel = document.createElement("div");
shortLabel.classList.add("unit-short-label");
shortLabel.innerText = this.getDatabase()?.getByName(this.getBaseData().name)?.shortLabel || "";
el.append(shortLabel);
}
getHotgroup() {
return this.#hotgroup;
}
// Fuel indicator
if (this.getIconOptions().showFuel) {
var fuelIndicator = document.createElement("div");
fuelIndicator.classList.add("unit-fuel");
var fuelLevel = document.createElement("div");
fuelLevel.classList.add("unit-fuel-level");
fuelIndicator.appendChild(fuelLevel);
el.append(fuelIndicator);
}
setHighlighted(highlighted: boolean) {
this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-highlighted", highlighted);
this.#highlighted = highlighted;
}
// Ammo indicator
if (this.getIconOptions().showAmmo){
var ammoIndicator = document.createElement("div");
ammoIndicator.classList.add("unit-ammo");
for (let i = 0; i <= 3; i++)
ammoIndicator.appendChild(document.createElement("div"));
el.append(ammoIndicator);
}
getHighlighted() {
return this.#highlighted;
// Unit summary
if (this.getIconOptions().showSummary) {
var summary = document.createElement("div");
summary.classList.add("unit-summary");
var callsign = document.createElement("div");
callsign.classList.add("unit-callsign");
callsign.innerText = this.getBaseData().unitName;
var altitude = document.createElement("div");
altitude.classList.add("unit-altitude");
var speed = document.createElement("div");
speed.classList.add("unit-speed");
summary.appendChild(callsign);
summary.appendChild(altitude);
summary.appendChild(speed);
el.appendChild(summary);
}
this.getElement()?.appendChild(el);
}
/********************** Visibility *************************/
updateVisibility() {
this.setHidden(document.body.getAttribute(`data-hide-${this.getMissionData().coalition}`) != null ||
document.body.getAttribute(`data-hide-${this.getMarkerCategory()}`) != null ||
!this.getBaseData().alive)
var hidden = false;
const hiddenUnits = getUnitsManager().getHiddenTypes();
if (this.getMissionData().flags.Human && hiddenUnits.includes("human"))
hidden = true;
else if (this.getBaseData().AI == false && hiddenUnits.includes("dcs"))
hidden = true;
else if (hiddenUnits.includes(this.getMarkerCategory()))
hidden = true;
else if (hiddenUnits.includes(this.getMissionData().coalition))
hidden = true;
this.setHidden(hidden || !this.getBaseData().alive);
}
setHidden(hidden: boolean) {
@@ -430,18 +554,18 @@ export class Unit extends Marker {
}
#onContextMenu(e: any) {
var options: { [key: string]: string } = {};
var options: {[key: string]: {text: string, tooltip: string}} = {};
options["Center"] = `<div id="center-map">Center map</div>`;
options["center-map"] = {text: "Center map", tooltip: "Center the map on the unit and follow it"};
if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().length == 1 && (getUnitsManager().getSelectedUnits().includes(this)))) {
options['Attack'] = `<div id="attack">Attack</div>`;
options["attack"] = {text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons"};
if (getUnitsManager().getSelectedUnitsType() === "Aircraft")
options['Follow'] = `<div id="follow">Follow</div>`;
options["follow"] = {text: "Follow", tooltip: "Follow the unit at a user defined distance and position"};;
}
else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0) {
if (this.getBaseData().category == "Aircraft") {
options["Refuel"] = `<div id="refuel">Refuel</div>`; // TODO Add some way of knowing which aircraft can AAR
options["refuel"] = {text: "AAR Refuel", tooltip: "Refuel unit at the nearest AAR Tanker. If no tanker is available the unit will RTB."}; // TODO Add some way of knowing which aircraft can AAR
}
}
@@ -455,28 +579,28 @@ export class Unit extends Marker {
}
#executeAction(e: any, action: string) {
if (action === "Center")
if (action === "center-map")
getMap().centerOnUnit(this.ID);
if (action === "Attack")
if (action === "attack")
getUnitsManager().selectedUnitsAttackUnit(this.ID);
else if (action === "Refuel")
else if (action === "refuel")
getUnitsManager().selectedUnitsRefuel();
else if (action === "Follow")
else if (action === "follow")
this.#showFollowOptions(e);
}
#showFollowOptions(e: any) {
var options: { [key: string]: string } = {};
var options: {[key: string]: {text: string, tooltip: string}} = {};
options = {
'Trail': `<div id="trail">Trail</div>`,
'Echelon (LH)': `<div id="echelon-lh">Echelon (left)</div>`,
'Echelon (RH)': `<div id="echelon-rh">Echelon (right)</div>`,
'Line abreast (LH)': `<div id="line-abreast">Line abreast (left)</div>`,
'Line abreast (RH)': `<div id="line-abreast">Line abreast (right)</div>`,
'Front': `<div id="front">In front</div>`,
'Diamond': `<div id="diamond">Diamond</div>`,
'Custom': `<div id="custom">Custom</div>`
'trail': {text: "Trail", tooltip: "Follow unit in trail formation"},
'echelon-lh': {text: "Echelon (LH)", tooltip: "Follow unit in echelon left formation"},
'echelon-rh': {text: "Echelon (RH)", tooltip: "Follow unit in echelon right formation"},
'line-abreast-lh': {text: "Line abreast (LH)", tooltip: "Follow unit in line abreast left formation"},
'line-abreast-rh': {text: "Line abreast (RH)", tooltip: "Follow unit in line abreast right formation"},
'front': {text: "Front", tooltip: "Fly in front of unit"},
'diamond': {text: "Diamond", tooltip: "Follow unit in diamond formation"},
'custom': {text: "Custom", tooltip: "Set a custom formation position"},
}
getMap().getUnitContextMenu().setOptions(options, (option: string) => {
@@ -487,7 +611,7 @@ export class Unit extends Marker {
}
#applyFollowOptions(action: string) {
if (action === "Custom") {
if (action === "custom") {
document.getElementById("custom-formation-dialog")?.classList.remove("hide");
getMap().getUnitContextMenu().setCustomFormationCallback((offset: { x: number, y: number, z: number }) => {
getUnitsManager().selectedUnitsFollowUnit(this.ID, offset);
@@ -543,18 +667,18 @@ export class Unit extends Marker {
element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.getBaseData().alive);
/* Set current unit state */
if (this.getMissionData().flags.Human) // Unit is human
if (this.getMissionData().flags.Human) // Unit is human
element.querySelector(".unit")?.setAttribute("data-state", "human");
else if (!this.getBaseData().AI) // Unit is under DCS control (not Olympus)
else if (!this.getBaseData().AI) // Unit is under DCS control (not Olympus)
element.querySelector(".unit")?.setAttribute("data-state", "dcs");
else // Unit is under Olympus control
else // Unit is under Olympus control
element.querySelector(".unit")?.setAttribute("data-state", this.getTaskData().currentState.toLowerCase());
/* Set altitude and speed */
if (element.querySelector(".unit-altitude"))
(<HTMLElement>element.querySelector(".unit-altitude")).innerText = "FL" + String(Math.floor(this.getFlightData().altitude / 0.3048 / 100));
if (element.querySelector(".unit-speed"))
(<HTMLElement>element.querySelector(".unit-speed")).innerHTML = String(Math.floor(this.getFlightData().speed * 1.94384));
(<HTMLElement>element.querySelector(".unit-speed")).innerText = String(Math.floor(this.getFlightData().speed * 1.94384));
/* Rotate elements according to heading */
element.querySelectorAll("[data-rotate-to-heading]").forEach(el => {
@@ -563,7 +687,7 @@ export class Unit extends Marker {
el.setAttribute("style", currentStyle + `transform:rotate(${headingDeg}deg);`);
});
/* Turn on ordnance indicators */
/* Turn on ammo indicators */
var hasFox1 = element.querySelector(".unit")?.hasAttribute("data-has-fox-1");
var hasFox2 = element.querySelector(".unit")?.hasAttribute("data-has-fox-2");
var hasFox3 = element.querySelector(".unit")?.hasAttribute("data-has-fox-3");
@@ -646,27 +770,25 @@ export class Unit extends Marker {
}
#drawTargets() {
for (let typeIndex in this.getMissionData().targets) {
for (let index in this.getMissionData().targets[typeIndex]) {
var targetData = this.getMissionData().targets[typeIndex][index];
var target = getUnitsManager().getUnitByID(targetData.object["id_"])
if (target != null) {
var startLatLng = new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)
var endLatLng = new LatLng(target.getFlightData().latitude, target.getFlightData().longitude)
for (let index in this.getMissionData().targets) {
var targetData = this.getMissionData().targets[index];
var target = getUnitsManager().getUnitByID(targetData.object["id_"])
if (target != null) {
var startLatLng = new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)
var endLatLng = new LatLng(target.getFlightData().latitude, target.getFlightData().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: 0.4, smoothFactor: 1 });
targetPolyline.addTo(getMap());
this.#targetsPolylines.push(targetPolyline)
}
var color;
if (targetData.detectionMethod === "RADAR")
color = "#FFFF00";
else if (targetData.detectionMethod === "VISUAL")
color = "#FF00FF";
else if (targetData.detectionMethod === "RWR")
color = "#00FF00";
else
color = "#FFFFFF";
var targetPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 0.4, smoothFactor: 1 });
targetPolyline.addTo(getMap());
this.#targetsPolylines.push(targetPolyline)
}
}
}
@@ -679,7 +801,19 @@ export class Unit extends Marker {
}
export class AirUnit extends Unit {
getIconOptions() {
return {
showState: true,
showVvi: true,
showHotgroup: true,
showUnitIcon: true,
showShortLabel: true,
showFuel: true,
showAmmo: true,
showSummary: true,
rotateToHeading: false
};
}
}
export class Aircraft extends AirUnit {
@@ -687,37 +821,13 @@ export class Aircraft extends AirUnit {
super(ID, data);
}
getMarkerHTML() {
return `<div class="unit" data-object="unit-aircraft" data-coalition="${this.getMissionData().coalition}">
<div class="unit-selected-spotlight"></div>
<div class="unit-marker-border"></div>
<div class="unit-state"></div>
<div class="unit-vvi" data-rotate-to-heading></div>
<div class="unit-hotgroup">
<div class="unit-hotgroup-id"></div>
</div>
<div class="unit-marker"></div>
<div class="unit-short-label">${aircraftDatabase.getByName(this.getBaseData().name)?.shortLabel || ""}</div>
<div class="unit-fuel">
<div class="unit-fuel-level" style="width:100%;"></div>
</div>
<div class="unit-ammo">
<div class="unit-ammo-fox-1"></div>
<div class="unit-ammo-fox-2"></div>
<div class="unit-ammo-fox-3"></div>
<div class="unit-ammo-other"></div>
</div>
<div class="unit-summary">
<div class="unit-callsign">${this.getBaseData().unitName}</div>
<div class="unit-altitude"></div>
<div class="unit-speed"></div>
</div>
</div>`
}
getMarkerCategory() {
return "aircraft";
}
getDatabase(): UnitDatabase | null {
return aircraftDatabase;
}
}
export class Helicopter extends AirUnit {
@@ -725,7 +835,7 @@ export class Helicopter extends AirUnit {
super(ID, data);
}
getVisibilityCategory() {
getMarkerCategory() {
return "helicopter";
}
}
@@ -735,24 +845,30 @@ export class GroundUnit extends Unit {
super(ID, data);
}
getMarkerHTML() {
var role = groundUnitsDatabase.getByName(this.getBaseData().name)?.loadouts[0].roles[0];
return `<div class="unit" data-object="unit-${this.getMarkerCategory()}" data-coalition="${this.getMissionData().coalition}">
<div class="unit-selected-spotlight"></div>
<div class="unit-marker"></div>
<div class="unit-short-label">${role?.substring(0, 1)?.toUpperCase() || ""}</div>
<div class="unit-hotgroup">
<div class="unit-hotgroup-id"></div>
</div>
</div>`
getIconOptions() {
return {
showState: true,
showVvi: false,
showHotgroup: true,
showUnitIcon: true,
showShortLabel: true,
showFuel: false,
showAmmo: false,
showSummary: false,
rotateToHeading: false
};
}
getMarkerCategory() {
// TODO this is very messy
var role = groundUnitsDatabase.getByName(this.getBaseData().name)?.loadouts[0].roles[0];
var markerCategory = (role === "SAM") ? "sam" : "groundunit";
var markerCategory = (role === "SAM") ? "groundunit-sam" : "groundunit-other";
return markerCategory;
}
getDatabase(): UnitDatabase | null {
return groundUnitsDatabase;
}
}
export class NavyUnit extends Unit {
@@ -760,6 +876,20 @@ export class NavyUnit extends Unit {
super(ID, data);
}
getIconOptions() {
return {
showState: true,
showVvi: false,
showHotgroup: true,
showUnitIcon: true,
showShortLabel: true,
showFuel: false,
showAmmo: false,
showSummary: false,
rotateToHeading: false
};
}
getMarkerCategory() {
return "navyunit";
}
@@ -771,14 +901,19 @@ export class Weapon extends Unit {
this.setSelectable(false);
}
getMarkerHTML(): string {
return `<div class="unit" data-object="unit-${this.getMarkerCategory()}" data-coalition="${this.getMissionData().coalition}">
<div class="unit-selected-spotlight"></div>
<div class="unit-marker" data-rotate-to-heading></div>
<div class="unit-short-label"></div>
</div>`
getIconOptions() {
return {
showState: false,
showVvi: false,
showHotgroup: false,
showUnitIcon: true,
showShortLabel: false,
showFuel: false,
showAmmo: false,
showSummary: false,
rotateToHeading: true
};
}
}
export class Missile extends Weapon {

View File

@@ -1,21 +1,16 @@
export class UnitDatabase {
blueprints: {[key: string]: UnitBlueprint} = {};
blueprints: { [key: string]: UnitBlueprint } = {};
constructor()
{
constructor() {
}
/* Returns a list of all possible roles in a database */
getRoles()
{
getRoles() {
var roles: string[] = [];
for (let unit in this.blueprints)
{
for (let loadout of this.blueprints[unit].loadouts)
{
for (let role of loadout.roles)
{
for (let unit in this.blueprints) {
for (let loadout of this.blueprints[unit].loadouts) {
for (let role of loadout.roles) {
if (role !== "" && !roles.includes(role))
roles.push(role);
}
@@ -25,18 +20,15 @@ export class UnitDatabase {
}
/* Gets a specific blueprint by name */
getByName(name: string)
{
getByName(name: string) {
if (name in this.blueprints)
return this.blueprints[name];
return null;
}
/* Gets a specific blueprint by label */
getByLabel(label: string)
{
for (let unit in this.blueprints)
{
getByLabel(label: string) {
for (let unit in this.blueprints) {
if (this.blueprints[unit].label === label)
return this.blueprints[unit];
}
@@ -44,15 +36,11 @@ export class UnitDatabase {
}
/* Get all blueprints by role */
getByRole(role: string)
{
getByRole(role: string) {
var units = [];
for (let unit in this.blueprints)
{
for (let loadout of this.blueprints[unit].loadouts)
{
if (loadout.roles.includes(role) || loadout.roles.includes(role.toLowerCase()))
{
for (let unit in this.blueprints) {
for (let loadout of this.blueprints[unit].loadouts) {
if (loadout.roles.includes(role) || loadout.roles.includes(role.toLowerCase())) {
units.push(this.blueprints[unit])
break;
}
@@ -62,13 +50,10 @@ export class UnitDatabase {
}
/* Get the names of all the loadouts for a specific unit and for a specific role */
getLoadoutNamesByRole(name: string, role: string)
{
getLoadoutNamesByRole(name: string, role: string) {
var loadouts = [];
for (let loadout of this.blueprints[name].loadouts)
{
if (loadout.roles.includes(role) || loadout.roles.includes(""))
{
for (let loadout of this.blueprints[name].loadouts) {
if (loadout.roles.includes(role) || loadout.roles.includes("")) {
loadouts.push(loadout.name)
}
}
@@ -76,10 +61,8 @@ export class UnitDatabase {
}
/* Get the loadout content from the unit name and loadout name */
getLoadoutByName(name: string, loadoutName: string)
{
for (let loadout of this.blueprints[name].loadouts)
{
getLoadoutByName(name: string, loadoutName: string) {
for (let loadout of this.blueprints[name].loadouts) {
if (loadout.name === loadoutName)
return loadout;
}

View File

@@ -3,13 +3,14 @@ import { getHotgroupPanel, getInfoPopup, getMap, getUnitDataTable } from "..";
import { Unit } from "./unit";
import { cloneUnit } from "../server/server";
import { IDLE, MOVE_UNIT } from "../map/map";
import { keyEventWasInInput } from "../other/utils";
import { deg2rad, keyEventWasInInput, latLngToMercator, mercatorToLatLng } from "../other/utils";
export class UnitsManager {
#units: { [ID: number]: Unit };
#copiedUnits: Unit[];
#selectionEventDisabled: boolean = false;
#pasteDisabled: boolean = false;
#hiddenTypes: string[] = [];
constructor() {
this.#units = {};
@@ -46,7 +47,7 @@ export class UnitsManager {
}
getUnitsByHotgroup(hotgroup: number) {
return Object.values(this.#units).filter((unit: Unit) => {return unit.getBaseData().alive && unit.getHotgroup() == hotgroup});
return Object.values(this.#units).filter((unit: Unit) => { return unit.getBaseData().alive && unit.getHotgroup() == hotgroup });
}
addUnit(ID: number, data: UnitData) {
@@ -62,6 +63,7 @@ export class UnitsManager {
}
update(data: UnitsData) {
var updatedUnits: Unit[] = [];
Object.keys(data.units)
.filter((ID: string) => !(ID in this.#units))
.reduce((timeout: number, ID: string) => {
@@ -75,7 +77,29 @@ export class UnitsManager {
Object.keys(data.units)
.filter((ID: string) => ID in this.#units)
.forEach((ID: string) => this.#units[parseInt(ID)]?.setData(data.units[ID]));
.forEach((ID: string) => {
updatedUnits.push(this.#units[parseInt(ID)]);
this.#units[parseInt(ID)]?.setData(data.units[ID])
});
this.getSelectedUnits().forEach((unit: Unit) => {
if (!updatedUnits.includes(unit))
unit.setData({})
});
}
setHiddenType(key: string, value: boolean) {
if (value) {
if (this.#hiddenTypes.includes(key))
delete this.#hiddenTypes[this.#hiddenTypes.indexOf(key)];
}
else
this.#hiddenTypes.push(key);
Object.values(this.getUnits()).forEach((unit: Unit) => unit.updateVisibility());
}
getHiddenTypes() {
return this.#hiddenTypes;
}
selectUnit(ID: number, deselectAllUnits: boolean = true) {
@@ -96,7 +120,7 @@ export class UnitsManager {
}
}
getSelectedUnits(options?: {excludeHumans?: boolean}) {
getSelectedUnits(options?: { excludeHumans?: boolean }) {
var selectedUnits = [];
for (let ID in this.#units) {
if (this.#units[ID].getSelected()) {
@@ -105,7 +129,7 @@ export class UnitsManager {
}
if (options) {
if (options.excludeHumans)
selectedUnits = selectedUnits.filter((unit: Unit) => {return !unit.getMissionData().flags.Human});
selectedUnits = selectedUnits.filter((unit: Unit) => { return !unit.getMissionData().flags.Human });
}
return selectedUnits;
}
@@ -162,8 +186,16 @@ export class UnitsManager {
};
/*********************** Actions on selected units ************************/
selectedUnitsAddDestination(latlng: L.LatLng) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
selectedUnitsAddDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
/* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative distances */
var unitDestinations: { [key: number]: LatLng } = {};
if (mantainRelativePosition)
unitDestinations = this.selectedUnitsComputeGroupDestination(latlng, rotation);
else
selectedUnits.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng });
for (let idx in selectedUnits) {
const unit = selectedUnits[idx];
/* If a unit is following another unit, and that unit is also selected, send the command to the followed unit */
@@ -174,14 +206,17 @@ export class UnitsManager {
else
unit.addDestination(latlng);
}
else
unit.addDestination(latlng);
else {
if (unit.ID in unitDestinations)
unit.addDestination(unitDestinations[unit.ID]);
}
}
this.#showActionMessage(selectedUnits, " new destination added");
}
selectedUnitsClearDestinations() {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
const unit = selectedUnits[idx];
if (unit.getTaskData().currentState === "Follow") {
@@ -197,7 +232,7 @@ export class UnitsManager {
}
selectedUnitsLandAt(latlng: LatLng) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].landAt(latlng);
}
@@ -205,21 +240,21 @@ export class UnitsManager {
}
selectedUnitsChangeSpeed(speedChange: string) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].changeSpeed(speedChange);
}
}
selectedUnitsChangeAltitude(altitudeChange: string) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].changeAltitude(altitudeChange);
}
}
selectedUnitsSetSpeed(speed: number) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setSpeed(speed);
}
@@ -227,7 +262,7 @@ export class UnitsManager {
}
selectedUnitsSetAltitude(altitude: number) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setAltitude(altitude);
}
@@ -235,7 +270,7 @@ export class UnitsManager {
}
selectedUnitsSetROE(ROE: string) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setROE(ROE);
}
@@ -243,7 +278,7 @@ export class UnitsManager {
}
selectedUnitsSetReactionToThreat(reactionToThreat: string) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setReactionToThreat(reactionToThreat);
}
@@ -251,7 +286,7 @@ export class UnitsManager {
}
selectedUnitsSetEmissionsCountermeasures(emissionCountermeasure: string) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].setEmissionsCountermeasures(emissionCountermeasure);
}
@@ -260,7 +295,7 @@ export class UnitsManager {
selectedUnitsAttackUnit(ID: number) {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].attackUnit(ID);
}
@@ -276,7 +311,7 @@ export class UnitsManager {
}
selectedUnitsRefuel() {
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
for (let idx in selectedUnits) {
selectedUnits[idx].refuel();
}
@@ -290,15 +325,15 @@ export class UnitsManager {
// Y: top-bottom, positive top
// Z: left-right, positive right
offset = { "x": 0, "y": 0, "z": 0 };
if (formation === "Trail") { offset.x = -50; offset.y = -30; offset.z = 0; }
else if (formation === "Echelon (LH)") { offset.x = -50; offset.y = -10; offset.z = -50; }
else if (formation === "Echelon (RH)") { offset.x = -50; offset.y = -10; offset.z = 50; }
else if (formation === "Line abreast (RH)") { offset.x = 0; offset.y = 0; offset.z = 50; }
else if (formation === "Line abreast (LH)") { offset.x = 0; offset.y = 0; offset.z = -50; }
else if (formation === "Front") { offset.x = 100; offset.y = 0; offset.z = 0; }
if (formation === "trail") { offset.x = -50; offset.y = -30; offset.z = 0; }
else if (formation === "echelon-lh") { offset.x = -50; offset.y = -10; offset.z = -50; }
else if (formation === "echelon-rh") { offset.x = -50; offset.y = -10; offset.z = 50; }
else if (formation === "line-abreast-rh") { offset.x = 0; offset.y = 0; offset.z = 50; }
else if (formation === "line-abreast-lh") { offset.x = 0; offset.y = 0; offset.z = -50; }
else if (formation === "front") { offset.x = 100; offset.y = 0; offset.z = 0; }
else offset = undefined;
}
var selectedUnits = this.getSelectedUnits({excludeHumans: true});
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
var count = 1;
var xr = 0; var yr = 1; var zr = -1;
var layer = 1;
@@ -309,7 +344,7 @@ export class UnitsManager {
unit.followUnit(ID, { "x": offset.x * count, "y": offset.y * count, "z": offset.z * count });
else {
/* More complex formations with variable offsets */
if (formation === "Diamond") {
if (formation === "diamond") {
var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4);
var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4);
unit.followUnit(ID, { "x": -yl * 50, "y": zr * 10, "z": xl * 50 });
@@ -326,14 +361,12 @@ export class UnitsManager {
this.#showActionMessage(selectedUnits, `following unit ${this.getUnitByID(ID)?.getBaseData().unitName}`);
}
selectedUnitsSetHotgroup(hotgroup: number)
{
selectedUnitsSetHotgroup(hotgroup: number) {
this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setHotgroup(null));
this.selectedUnitsAddToHotgroup(hotgroup);
}
selectedUnitsAddToHotgroup(hotgroup: number)
{
selectedUnitsAddToHotgroup(hotgroup: number) {
var selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
selectedUnits[idx].setHotgroup(hotgroup);
@@ -342,6 +375,37 @@ export class UnitsManager {
getHotgroupPanel().refreshHotgroups();
}
selectedUnitsComputeGroupDestination(latlng: LatLng, rotation: number) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true });
/* Compute the center of the group */
var center = { x: 0, y: 0 };
selectedUnits.forEach((unit: Unit) => {
var mercator = latLngToMercator(unit.getFlightData().latitude, unit.getFlightData().longitude);
center.x += mercator.x / selectedUnits.length;
center.y += mercator.y / selectedUnits.length;
});
/* Compute the distances from the center of the group */
var unitDestinations: { [key: number]: LatLng } = {};
selectedUnits.forEach((unit: Unit) => {
var mercator = latLngToMercator(unit.getFlightData().latitude, unit.getFlightData().longitude);
var distancesFromCenter = { dx: mercator.x - center.x, dy: mercator.y - center.y };
/* Rotate the distance according to the group rotation */
var rotatedDistancesFromCenter: { dx: number, dy: number } = { dx: 0, dy: 0 };
rotatedDistancesFromCenter.dx = distancesFromCenter.dx * Math.cos(deg2rad(rotation)) - distancesFromCenter.dy * Math.sin(deg2rad(rotation));
rotatedDistancesFromCenter.dy = distancesFromCenter.dx * Math.sin(deg2rad(rotation)) + distancesFromCenter.dy * Math.cos(deg2rad(rotation));
/* Compute the final position of the unit */
var destMercator = latLngToMercator(latlng.lat, latlng.lng); // Convert destination point to mercator
var unitMercator = { x: destMercator.x + rotatedDistancesFromCenter.dx, y: destMercator.y + rotatedDistancesFromCenter.dy }; // Compute final position of this unit in mercator coordinates
var unitLatLng = mercatorToLatLng(unitMercator.x, unitMercator.y);
unitDestinations[unit.ID] = new LatLng(unitLatLng.lat, unitLatLng.lng);
});
return unitDestinations;
}
/***********************************************/
copyUnits() {
this.#copiedUnits = this.getSelectedUnits(); /* Can be applied to humans too */