Merge branch 'main' into 357-map-context-switching

This commit is contained in:
PeekabooSteam
2023-10-31 20:30:18 +00:00
committed by GitHub
37 changed files with 16091 additions and 10175 deletions

View File

@@ -104,26 +104,26 @@ export const mapBounds = {
export const mapLayers = {
"ArcGIS Satellite": {
urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
maxZoom: 20,
minZoom: 1,
maxZoom: 16,
attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, GetApp().getMap()ping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community"
},
"USGS Topo": {
urlTemplate: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
minZoom: 1,
maxZoom: 20,
maxZoom: 16,
attribution: 'Tiles courtesy of the <a href="https://usgs.gov/">U.S. Geological Survey</a>'
},
"OpenStreetMap Mapnik": {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 19,
maxZoom: 16,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
},
"OPENVKarte": {
urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 18,
maxZoom: 16,
attribution: 'Map <a href="https://memomaps.de/">memomaps.de</a> <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
},
"Esri.DeLorme": {
@@ -135,7 +135,7 @@ export const mapLayers = {
"CyclOSM": {
urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 20,
maxZoom: 16,
attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}
}
@@ -209,4 +209,7 @@ export const MGRS_PRECISION_10KM = 2;
export const MGRS_PRECISION_1KM = 3;
export const MGRS_PRECISION_100M = 4;
export const MGRS_PRECISION_10M = 5;
export const MGRS_PRECISION_1M = 6;
export const MGRS_PRECISION_1M = 6;
export const DELETE_CYCLE_TIME = 0.05;
export const DELETE_SLOW_THRESHOLD = 50;

View File

@@ -18,6 +18,7 @@ export class MapContextMenu extends ContextMenu {
#groundUnitSpawnMenu: GroundUnitSpawnMenu;
#navyUnitSpawnMenu: NavyUnitSpawnMenu;
#coalitionArea: CoalitionArea | null = null;
#selectedUnitCategory: string = "";
/**
*
@@ -39,9 +40,10 @@ export class MapContextMenu extends ContextMenu {
/* Event listeners */
document.addEventListener("mapContextMenuShow", (e: any) => {
if (this.getVisibleSubMenu() !== e.detail.type)
if (this.getVisibleSubMenu() !== e.detail.type) {
this.#showSubMenu(e.detail.type);
else
this.#selectedUnitCategory = e.detail.type;
} else
this.#hideSubMenus(e.detail.type);
});
@@ -65,9 +67,9 @@ export class MapContextMenu extends ContextMenu {
}
});
document.addEventListener("commandModeOptionsChanged", (e: any) => {
//this.#refreshOptions();
});
// document.addEventListener("commandModeOptionsChanged", (e: any) => {
// //this.#refreshOptions();
// });
this.#aircraftSpawnMenu.getContainer().addEventListener("resize", () => this.clip());
this.#helicopterSpawnMenu.getContainer().addEventListener("resize", () => this.clip());

View File

@@ -36,8 +36,21 @@ export class Dropdown {
return this.#container;
}
setOptions(optionsList: string[], sortAlphabetically: boolean = true) {
this.#optionsList = optionsList.sort();
setOptions(optionsList: string[], sort:""|"string"|"number" = "string") {
if ( sort === "number" ) {
this.#optionsList = optionsList.sort( (optionA:string, optionB:string) => {
const a = parseInt( optionA );
const b = parseInt( optionB );
if ( a > b )
return 1;
else
return ( b > a ) ? -1 : 0;
});
} else if ( sort === "string" ) {
this.#optionsList = optionsList.sort();
}
if (this.#optionsList.length == 0) {
optionsList = ["No options available"]
this.#value.innerText = "No options available";

View File

@@ -13,11 +13,8 @@ import { navyUnitDatabase } from "../unit/databases/navyunitdatabase";
import { UnitSpawnOptions, UnitSpawnTable } from "../interfaces";
export class UnitSpawnMenu {
#container: HTMLElement;
#unitDatabase: UnitDatabase;
#countryCodes: any;
#orderByRole: boolean;
spawnOptions: UnitSpawnOptions = {
protected showRangeCircles: boolean = false;
protected spawnOptions: UnitSpawnOptions = {
roleType: "",
name: "",
latlng: new LatLng(0, 0),
@@ -30,6 +27,11 @@ export class UnitSpawnMenu {
altitude: undefined
};
#container: HTMLElement;
#unitDatabase: UnitDatabase;
#countryCodes: any;
#orderByRole: boolean;
/* Controls */
#unitRoleTypeDropdown: Dropdown;
#unitLabelDropdown: Dropdown;
@@ -244,6 +246,10 @@ export class UnitSpawnMenu {
return this.#container;
}
getVisible() {
return !this.getContainer().classList.contains( "hide" );
}
reset() {
this.#deployUnitButtonEl.disabled = true;
this.#unitRoleTypeDropdown.reset();
@@ -286,9 +292,17 @@ export class UnitSpawnMenu {
showCirclesPreviews() {
this.clearCirclesPreviews();
if ( !this.showRangeCircles || this.spawnOptions.name === "" || !this.getVisible() ) {
return;
}
var acquisitionRange = this.#unitDatabase.getByName(this.spawnOptions.name)?.acquisitionRange ?? 0;
var engagementRange = this.#unitDatabase.getByName(this.spawnOptions.name)?.engagementRange ?? 0;
let acquisitionRange = this.#unitDatabase.getByName(this.spawnOptions.name)?.acquisitionRange ?? 0;
let engagementRange = this.#unitDatabase.getByName(this.spawnOptions.name)?.engagementRange ?? 0;
if ( acquisitionRange === 0 && engagementRange === 0 ) {
return;
}
this.#acquisitionCircle.setRadius(acquisitionRange);
this.#engagementCircle.setRadius(engagementRange);
@@ -343,10 +357,7 @@ export class UnitSpawnMenu {
setMaxUnitCount(maxUnitCount: number) {
/* Create the unit count options */
var countOptions: string[] = [];
for (let i = 1; i <= maxUnitCount; i++)
countOptions.push(i.toString());
this.#unitCountDropdown.setOptions(countOptions);
this.#unitCountDropdown.setOptions( [...Array(maxUnitCount).keys()].map( n => (n+1).toString() ), "number");
this.#unitCountDropdown.selectValue(0);
}
@@ -572,6 +583,9 @@ export class HelicopterSpawnMenu extends UnitSpawnMenu {
}
export class GroundUnitSpawnMenu extends UnitSpawnMenu {
protected showRangeCircles: boolean = true;
/**
*
* @param ID - the ID of the HTML element which will contain the context menu

View File

@@ -0,0 +1,19 @@
import { Panel } from "../panels/panel";
export class Dialog extends Panel {
constructor( element:string ) {
super( element );
}
hide() {
super.hide();
document.getElementById( "gray-out" )?.classList.toggle("hide", true);
}
show() {
super.show();
document.getElementById( "gray-out" )?.classList.toggle("hide", false);
}
}

View File

@@ -94,7 +94,7 @@ export class Map extends L.Map {
super(ID, {
zoomSnap: 0,
zoomDelta: 0.25,
preferCanvas: false,
preferCanvas: true,
doubleClickZoom: false,
zoomControl: false,
boxZoom: false,
@@ -400,6 +400,7 @@ export class Map extends L.Map {
this.options.scrollWheelZoom = undefined;
this.#centerUnit = null;
}
this.#updateCursor();
}
getCenterUnit() {
@@ -714,6 +715,8 @@ export class Map extends L.Map {
#panToUnit(unit: Unit) {
var unitPosition = new L.LatLng(unit.getPosition().lat, unit.getPosition().lng);
this.setView(unitPosition, this.getZoom(), { animate: false });
this.#updateCursor();
this.#updateDestinationCursors();
}
#getMinimapBoundaries() {

View File

@@ -37,6 +37,7 @@ export class OlympusApp {
/* Managers */
#contextManager!: ContextManager;
#dialogManager!: Manager;
#missionManager: MissionManager | null = null;
#panelsManager: Manager | null = null;
#pluginsManager: PluginsManager | null = null;
@@ -52,6 +53,10 @@ export class OlympusApp {
}
// TODO add checks on null
getDialogManager() {
return this.#dialogManager as Manager;
}
getMap() {
return this.#map as Map;
}
@@ -178,15 +183,14 @@ export class OlympusApp {
this.#map = new Map('map-container');
this.#serverManager = new ServerManager();
this.#unitsManager = new UnitsManager();
this.#weaponsManager = new WeaponsManager();
this.#missionManager = new MissionManager();
this.#panelsManager = new Manager();
this.#popupsManager = new Manager();
this.#serverManager = new ServerManager();
this.#shortcutManager = new ShortcutManager();
this.#toolbarsManager = new Manager();
this.#unitsManager = new UnitsManager();
this.#weaponsManager = new WeaponsManager();
// Panels
this.getPanelsManager()
@@ -200,7 +204,8 @@ export class OlympusApp {
.add("unitList", new UnitListPanel("unit-list-panel", "unit-list-panel-content"))
// Popups
this.getPopupsManager().add("infoPopup", new Popup("info-popup"));
this.getPopupsManager()
.add("infoPopup", new Popup("info-popup"));
// Toolbars
this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar"))
@@ -366,13 +371,13 @@ export class OlympusApp {
"altKey": false,
"callback": (ev: KeyboardEvent) => {
if (ev.ctrlKey && ev.shiftKey)
this.getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5)));
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); // "Select hotgroup X in addition to any units already selected"
else if (ev.ctrlKey && !ev.shiftKey)
this.getUnitsManager().selectedUnitsSetHotgroup(parseInt(ev.code.substring(5)));
this.getUnitsManager().selectedUnitsSetHotgroup(parseInt(ev.code.substring(5))); // "These selected units are hotgroup X (forget any previous membership)"
else if (!ev.ctrlKey && ev.shiftKey)
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false);
this.getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5))); // "Add (append) these units to hotgroup X (in addition to any existing members)"
else
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)));
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it."
},
"code": code
});

View File

@@ -42,8 +42,8 @@ export class HotgroupPanel extends Panel {
this.getElement().appendChild(el);
el.addEventListener("click", () => {
getApp().getUnitsManager().selectUnitsByHotgroup(hotgroup);
el.addEventListener("click", ( ev:MouseEvent ) => {
getApp().getUnitsManager().selectUnitsByHotgroup(hotgroup, (!ev.ctrlKey));
});
el.addEventListener("mouseover", () => {

View File

@@ -541,14 +541,15 @@ export class ServerManager {
}
setConnected(newConnected: boolean) {
if (this.#connected != newConnected)
if (this.#connected != newConnected) {
newConnected ? (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Connected to DCS Olympus server") : (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Disconnected from DCS Olympus server");
this.#connected = newConnected;
if (this.#connected) {
document.querySelector("#splash-screen")?.classList.add("hide");
document.querySelector("#gray-out")?.classList.add("hide");
if (newConnected) {
document.getElementById("splash-screen")?.classList.add("hide");
document.getElementById("gray-out")?.classList.add("hide");
}
}
this.#connected = newConnected;
}
getConnected() {

View File

@@ -1361,7 +1361,7 @@ export class AirUnit extends Unit {
if (getApp().getUnitsManager().getSelectedUnits().length == 1) {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
} else {
/* Provision */
options["follow"] = { text: "Follow", tooltip: "Follow the unit at a user defined distance and position", type: "or" };
}
options["refuel"] = { text: "Air to air refuel", tooltip: "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB.", type: "and" }; // TODO Add some way of knowing which aircraft can AAR

View File

@@ -4,7 +4,7 @@ import { Unit } from "./unit";
import { bearingAndDistanceToLatLng, deg2rad, getGroundElevation, getUnitDatabaseByCategory, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils";
import { CoalitionArea } from "../map/coalitionarea/coalitionarea";
import { groundUnitDatabase } from "./databases/groundunitdatabase";
import { DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT } from "../constants/constants";
import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT } from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { citiesDatabase } from "./citiesDatabase";
import { aircraftDatabase } from "./databases/aircraftdatabase";
@@ -14,35 +14,40 @@ import { TemporaryUnitMarker } from "../map/markers/temporaryunitmarker";
import { Popup } from "../popups/popup";
import { HotgroupPanel } from "../panels/hotgrouppanel";
import { Contact, UnitData, UnitSpawnTable } from "../interfaces";
import { Dialog } from "../dialog/dialog";
/** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only
* result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows
* to avoid client/server and client/client inconsistencies.
*/
export class UnitsManager {
#units: { [ID: number]: Unit };
#copiedUnits: UnitData[];
#selectionEventDisabled: boolean = false;
#deselectionEventDisabled: boolean = false;
#requestDetectionUpdate: boolean = false;
#selectionEventDisabled: boolean = false;
#slowDeleteDialog!:Dialog;
#units: { [ID: number]: Unit };
constructor() {
this.#units = {};
this.#copiedUnits = [];
this.#units = {};
document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) });
document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true });
document.addEventListener('copy', () => this.selectedUnitsCopy());
document.addEventListener('paste', () => this.pasteUnits());
document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail));
document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail));
document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete());
document.addEventListener('explodeSelectedUnits', () => this.selectedUnitsDelete(true));
document.addEventListener('keyup', (event) => this.#onKeyUp(event));
document.addEventListener('exportToFile', () => this.exportToFile());
document.addEventListener('importFromFile', () => this.importFromFile());
document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true });
document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) });
document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.selectedUnitsChangeSpeed(e.detail.type) });
document.addEventListener('keyup', (event) => this.#onKeyUp(event));
document.addEventListener('paste', () => this.pasteUnits());
document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.selectedUnitsChangeAltitude(e.detail.type) });
document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.selectedUnitsChangeSpeed(e.detail.type) });
document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail));
document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail));
this.#slowDeleteDialog = new Dialog( "slow-delete-dialog" );
}
/**
@@ -328,7 +333,7 @@ export class UnitsManager {
const unit = selectedUnits[idx];
/* If a unit is following another unit, and that unit is also selected, send the command to the followed ("leader") unit */
if (unit.getState() === "Follow") {
if (unit.getState() === "follow") {
const leader = this.getUnitByID(unit.getLeaderID())
if (leader && leader.getSelected())
leader.addDestination(latlng);
@@ -351,7 +356,7 @@ export class UnitsManager {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
for (let idx in selectedUnits) {
const unit = selectedUnits[idx];
if (unit.getState() === "Follow") {
if (unit.getState() === "follow") {
const leader = this.getUnitByID(unit.getLeaderID())
if (leader && leader.getSelected())
leader.clearDestinations();
@@ -561,29 +566,32 @@ export class UnitsManager {
}
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
var count = 1;
var xr = 0; var yr = 1; var zr = -1;
var layer = 1;
for (let idx in selectedUnits) {
var unit = selectedUnits[idx];
if (offset != undefined)
/* Offset is set, apply it */
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") {
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 });
if (unit.ID !== ID) {
if (offset != undefined)
/* Offset is set, apply it */
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") {
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 });
if (yr == 0) { layer++; xr = 0; yr = layer; zr = -layer; }
else {
if (xr < layer) { xr++; zr--; }
else { yr--; zr++; }
if (yr == 0) { layer++; xr = 0; yr = layer; zr = -layer; }
else {
if (xr < layer) { xr++; zr--; }
else { yr--; zr++; }
}
}
}
count++;
}
count++;
}
this.#showActionMessage(selectedUnits, `following unit ${this.getUnitByID(ID)?.getUnitName()}`);
}
@@ -635,7 +643,7 @@ export class UnitsManager {
try {
groundElevation = parseFloat(response);
} catch {
console.log("Simulate fire fight: could not retrieve ground elevation")
console.warn("Simulate fire fight: could not retrieve ground elevation")
}
for (let idx in selectedUnits) {
selectedUnits[idx].simulateFireFight(latlng, groundElevation);
@@ -751,14 +759,24 @@ export class UnitsManager {
return;
}
var immediate = false;
if (selectedUnits.length > 20)
immediate = confirm(`You are trying to delete ${selectedUnits.length} units, do you want to delete them immediately? This may cause lag for players.`)
for (let idx in selectedUnits) {
selectedUnits[idx].delete(explosion, immediate);
const doDelete = (explosion = false, immediate = false) => {
const selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
selectedUnits[idx].delete(explosion, immediate);
}
this.#showActionMessage(selectedUnits, `deleted`);
}
this.#showActionMessage(selectedUnits, `deleted`);
if (selectedUnits.length >= DELETE_SLOW_THRESHOLD)
this.#showSlowDeleteDialog(selectedUnits).then((action:any) => {
if (action === "delete-slow")
doDelete(explosion, false);
else if (action === "delete-immediate")
doDelete(explosion, true);
})
else
doDelete(explosion);
}
/** Compute the destinations of every unit in the selected units. This function preserves the relative positions of the units, and rotates the whole formation by rotation.
@@ -1080,4 +1098,32 @@ export class UnitsManager {
else if (units.length > 1)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`);
}
#showSlowDeleteDialog(selectedUnits:Unit[]) {
let button:HTMLButtonElement | null = null;
const deletionTime = Math.round( selectedUnits.length * DELETE_CYCLE_TIME ).toString();
const dialog = this.#slowDeleteDialog;
const element = dialog.getElement();
const listener = (ev:MouseEvent) => {
if (ev.target instanceof HTMLButtonElement && ev.target.matches("[data-action]"))
button = ev.target;
}
element.querySelectorAll(".deletion-count").forEach( el => el.innerHTML = selectedUnits.length.toString() );
element.querySelectorAll(".deletion-time").forEach( el => el.innerHTML = deletionTime );
dialog.show();
return new Promise((resolve) => {
element.addEventListener("click", listener);
const interval = setInterval(() => {
if (button instanceof HTMLButtonElement ) {
clearInterval(interval);
dialog.hide();
element.removeEventListener("click", listener);
resolve( button.getAttribute("data-action") );
}
}, 250);
});
}
}