Merge branch 'release-candidate' into features/redgreen-unit

This commit is contained in:
MarcoJayUsai 2025-03-22 09:18:16 +01:00
commit f7e9fc5cbc
16 changed files with 393 additions and 108 deletions

View File

@ -13,7 +13,7 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
fill="#3BB9FF"
fill="#none"
stroke="none"
stroke-width="2"
d="M45.7733 41.3423L25.9481 7.63951C25.5228 6.91648 24.4772 6.91646 24.0519 7.63951L4.22671 41.3423C3.79536 42.0756 4.32409 43 5.17484 43H44.8252C45.6759 43 46.2046 42.0756 45.7733 41.3423Z"

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -13,7 +13,7 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
fill="#3BB9FF"
fill="#none"
stroke="none"
stroke-width="2"
d="M45.7733 41.3423L25.9481 7.63951C25.5228 6.91648 24.4772 6.91646 24.0519 7.63951L4.22671 41.3423C3.79536 42.0756 4.32409 43 5.17484 43H44.8252C45.6759 43 46.2046 42.0756 45.7733 41.3423Z"

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -13,7 +13,7 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
fill="#3BB9FF"
fill="#none"
stroke="none"
stroke-width="2"
d="M45.7733 41.3423L25.9481 7.63951C25.5228 6.91648 24.4772 6.91646 24.0519 7.63951L4.22671 41.3423C3.79536 42.0756 4.32409 43 5.17484 43H44.8252C45.6759 43 46.2046 42.0756 45.7733 41.3423Z"

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -420,6 +420,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
hideChromeWarning: false,
hideSecureWarning: false,
showMissionDrawings: false,
clusterGroundUnits: true
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@ -517,7 +518,8 @@ export const DELETE_CYCLE_TIME = 0.05;
export const DELETE_SLOW_THRESHOLD = 50;
export const GROUPING_ZOOM_TRANSITION = 13;
export const SPOTS_EDIT_ZOOM_TRANSITION = 14;
export const CLUSTERING_ZOOM_TRANSITION = 13;
export const SPOTS_EDIT_ZOOM_TRANSITION = 13;
export const MAX_SHOTS_SCATTER = 3;
export const MAX_SHOTS_INTENSITY = 3;

View File

@ -14,6 +14,8 @@ import { Unit } from "./unit/unit";
import { LatLng } from "leaflet";
import { Weapon } from "./weapon/weapon";
const DEBUG = false;
export class BaseOlympusEvent {
static on(callback: () => void, singleShot = false) {
document.addEventListener(
@ -27,7 +29,7 @@ export class BaseOlympusEvent {
static dispatch() {
document.dispatchEvent(new CustomEvent(this.name));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -44,8 +46,8 @@ export class BaseUnitEvent {
static dispatch(unit: Unit) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } }));
console.log(`Event ${this.name} dispatched`);
console.log(unit);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(unit);
}
}
@ -62,7 +64,7 @@ export class BaseUnitsEvent {
static dispatch(units: Unit[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: units }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -81,8 +83,8 @@ export class AppStateChangedEvent {
static dispatch(state: OlympusState, subState: OlympusSubState) {
const detail = { state, subState };
document.dispatchEvent(new CustomEvent(this.name, { detail }));
console.log(`Event ${this.name} dispatched`);
console.log(`State: ${state} Substate: ${subState}`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`State: ${state} Substate: ${subState}`);
}
}
@ -99,8 +101,8 @@ export class ConfigLoadedEvent {
static dispatch(config: OlympusConfig) {
document.dispatchEvent(new CustomEvent(this.name, { detail: config }));
console.log(`Event ${this.name} dispatched`);
console.log(config);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(config);
}
}
@ -136,7 +138,7 @@ export class InfoPopupEvent {
static dispatch(messages: string[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { messages } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -155,7 +157,7 @@ export class ShortcutsChangedEvent {
static dispatch(shortcuts: { [key: string]: Shortcut }) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcuts } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -172,7 +174,7 @@ export class ShortcutChangedEvent {
static dispatch(shortcut: Shortcut) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -189,7 +191,7 @@ export class BindShortcutRequestEvent {
static dispatch(shortcut: Shortcut) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { shortcut } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -206,7 +208,7 @@ export class ModalEvent {
static dispatch(modal: boolean) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { modal } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -223,7 +225,7 @@ export class SessionDataChangedEvent {
static dispatch(sessionData: SessionData) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { sessionData } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -243,7 +245,7 @@ export class AdminPasswordChangedEvent {
static dispatch(password: string) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { password } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -278,24 +280,24 @@ export class HiddenTypesChangedEvent {
static dispatch(hiddenTypes: MapHiddenTypes) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { hiddenTypes } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
export class MapOptionsChangedEvent {
static on(callback: (mapOptions: MapOptions) => void, singleShot = false) {
static on(callback: (mapOptions: MapOptions, key: keyof MapOptions | undefined) => void, singleShot = false) {
document.addEventListener(
this.name,
(ev: CustomEventInit) => {
callback(ev.detail.mapOptions);
callback(ev.detail.mapOptions, ev.detail.key);
},
{ once: singleShot }
);
}
static dispatch(mapOptions: MapOptions) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { mapOptions } }));
console.log(`Event ${this.name} dispatched`);
static dispatch(mapOptions: MapOptions, key?: (keyof MapOptions) | undefined) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { mapOptions, key: key } }));
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -312,7 +314,7 @@ export class MapSourceChangedEvent {
static dispatch(source: string) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { source } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -329,7 +331,7 @@ export class CoalitionAreaSelectedEvent {
static dispatch(coalitionArea: CoalitionCircle | CoalitionPolygon | null) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { coalitionArea } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -348,7 +350,7 @@ export class CoalitionAreasChangedEvent {
static dispatch(coalitionAreas: (CoalitionCircle | CoalitionPolygon)[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { coalitionAreas } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -365,7 +367,7 @@ export class AirbaseSelectedEvent {
static dispatch(airbase: Airbase | null) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { airbase } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -382,7 +384,7 @@ export class SelectionEnabledChangedEvent {
static dispatch(enabled: boolean) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -399,7 +401,7 @@ export class PasteEnabledChangedEvent {
static dispatch(enabled: boolean) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -433,7 +435,7 @@ export class ContextActionSetChangedEvent {
static dispatch(contextActionSet: ContextActionSet | null) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { contextActionSet } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -450,7 +452,7 @@ export class ContextActionChangedEvent {
static dispatch(contextAction: ContextAction | null) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { contextAction } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -467,7 +469,7 @@ export class CopiedUnitsEvents {
static dispatch(unitsData: UnitData[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { unitsData } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -505,7 +507,7 @@ export class UnitExplosionRequestEvent {
static dispatch(units: Unit[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { units } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -522,7 +524,7 @@ export class FormationCreationRequestEvent {
static dispatch(leader: Unit, wingmen: Unit[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { leader, wingmen } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -539,7 +541,7 @@ export class MapContextMenuRequestEvent {
static dispatch(latlng: L.LatLng) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -556,7 +558,7 @@ export class UnitContextMenuRequestEvent {
static dispatch(unit: Unit) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -573,7 +575,7 @@ export class SpawnContextMenuRequestEvent {
static dispatch(latlng: L.LatLng) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -590,7 +592,7 @@ export class SpawnHeadingChangedEvent {
static dispatch(heading: number) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { heading } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -607,7 +609,7 @@ export class HotgroupsChangedEvent {
static dispatch(hotgroups: { [key: number]: Unit[] }) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { hotgroups } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -624,7 +626,7 @@ export class StarredSpawnsChangedEvent {
static dispatch(starredSpawns: { [key: number]: SpawnRequestTable }) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { starredSpawns } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -658,7 +660,7 @@ export class DrawingsInitEvent {
static dispatch(drawingsData: any /*TODO*/) {
document.dispatchEvent(new CustomEvent(this.name, {detail: drawingsData}));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -678,7 +680,7 @@ export class CommandModeOptionsChangedEvent {
static dispatch(options: CommandModeOptions) {
document.dispatchEvent(new CustomEvent(this.name, { detail: options }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -696,8 +698,8 @@ export class AudioSourcesChangedEvent {
static dispatch(audioSources: AudioSource[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSources } }));
console.log(`Event ${this.name} dispatched`);
console.log(audioSources);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(audioSources);
}
}
@ -714,8 +716,8 @@ export class AudioSinksChangedEvent {
static dispatch(audioSinks: AudioSink[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { audioSinks } }));
console.log(`Event ${this.name} dispatched`);
console.log(audioSinks);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(audioSinks);
}
}
@ -749,7 +751,7 @@ export class AudioManagerStateChangedEvent {
static dispatch(state: boolean) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { state } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -766,7 +768,7 @@ export class AudioManagerDevicesChangedEvent {
static dispatch(devices: MediaDeviceInfo[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { devices } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -783,7 +785,7 @@ export class AudioManagerInputChangedEvent {
static dispatch(input: MediaDeviceInfo) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { input } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -800,7 +802,7 @@ export class AudioManagerOutputChangedEvent {
static dispatch(output: MediaDeviceInfo) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { output } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -817,7 +819,7 @@ export class AudioManagerCoalitionChangedEvent {
static dispatch(coalition: Coalition) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { coalition } }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}
@ -887,6 +889,6 @@ export class WeaponsRefreshedEvent {
static dispatch(weapons: Weapon[]) {
document.dispatchEvent(new CustomEvent(this.name, { detail: weapons }));
console.log(`Event ${this.name} dispatched`);
if (DEBUG) console.log(`Event ${this.name} dispatched`);
}
}

View File

@ -172,6 +172,7 @@ export interface ObjectIconOptions {
showSummary: boolean;
showCallsign: boolean;
rotateToHeading: boolean;
showCluster: boolean;
}
export interface GeneralSettings {

View File

@ -779,7 +779,7 @@ export class Map extends L.Map {
setOption(key, value) {
this.#options[key] = value;
MapOptionsChangedEvent.dispatch(this.#options);
MapOptionsChangedEvent.dispatch(this.#options, key);
}
setOptions(options) {

View File

@ -0,0 +1,33 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
import { Coalition } from "../../types/types";
export class ClusterMarker extends CustomMarker {
#coalition: Coalition;
#numberOfUnits: number;
constructor(latlng: LatLngExpression, coalition: Coalition, numberOfUnits:number, options?: MarkerOptions) {
super(latlng, options);
this.setZIndexOffset(9999);
this.#coalition = coalition;
this.#numberOfUnits = numberOfUnits;
}
createIcon() {
this.setIcon(
new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 26],
className: "leaflet-cluster-marker",
})
);
var el = document.createElement("div");
el.classList.add("ol-cluster-icon");
el.classList.add(`${this.#coalition}`);
this.getElement()?.appendChild(el);
var span = document.createElement("span");
span.classList.add("ol-cluster-number");
span.textContent = `${this.#numberOfUnits}`;
el.appendChild(span);
}
}

View File

@ -93,6 +93,42 @@
translate: -1px 1px;
}
.unit-cluster {
border: 2px solid #272727;
border-radius: var(--border-radius-xs);
display: none;
height: 20px;
position: absolute;
translate: 70% -70%;
width: 30px;
}
.unit-cluster.red {
background-color: var(--unit-background-red);
}
.unit-cluster.blue {
background-color: var(--unit-background-blue);
}
.unit-cluster.neutral {
background-color: var(--unit-background-neutral);
}
.unit-cluster-id {
background-color: transparent;
color: #272727;
font-size: 12px;
font-weight: bolder;
translate: -3px -1px;
border-left: 3px solid #272727;
height: 50px;
padding-left: 4px;
text-align: center;
width: 50px;
}
.unit-icon {
height: var(--unit-height);
position: absolute;
@ -404,6 +440,7 @@
}
[data-object|="unit"][data-is-in-hotgroup] .unit-hotgroup,
[data-object|="unit"][data-is-cluster-leader] .unit-cluster,
[data-object|="unit"][data-is-selected] .unit-ammo,
[data-object|="unit"][data-is-selected] .unit-fuel,
[data-object|="unit"][data-is-selected] .unit-health,
@ -443,7 +480,7 @@
color: var(--secondary-red-text);
}
[data-object|="unit"][data-coalition="red"][data-is-selected] path {
[data-object|="unit"][data-coalition="red"][data-is-selected] path:nth-child(1) {
fill: var(--secondary-red-text);
}
@ -561,6 +598,8 @@
[data-object|="unit"][data-is-dead] .unit-vvi,
[data-object|="unit"][data-is-dead] .unit-hotgroup,
[data-object|="unit"][data-is-dead] .unit-hotgroup-id,
[data-object|="unit"][data-is-dead] .unit-cluster,
[data-object|="unit"][data-is-dead] .unit-cluster-id,
[data-object|="unit"][data-is-dead] .unit-state,
[data-object|="unit"][data-is-dead] .unit-fuel,
[data-object|="unit"][data-is-dead] .unit-health,

View File

@ -277,3 +277,34 @@ path.leaflet-interactive:focus {
.ol-arrow-icon svg path {
fill: #ffffff;
}
.ol-cluster-icon {
height: 100%;
width: 100%;
filter: drop-shadow(3px 3px 3px rgba(0, 0, 0, 0.2));
}
.ol-cluster-icon.neutral {
background-image: url("/images/markers/cluster-neutral.svg");;
}
.ol-cluster-icon.blue {
background-image: url("/images/markers/cluster-blue.svg");;
}
.ol-cluster-icon.red {
background-image: url("/images/markers/cluster-red.svg");;
}
.ol-cluster-number {
width: 100%;
height: 100%;
font-size: 14px;
font-weight: 700;
color: #272727;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -216,7 +216,7 @@ export class ServerManager {
}
getUnits(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, "arraybuffer", refresh);
this.GET(callback, errorCallback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, "arraybuffer", false);
}
getWeapons(callback: CallableFunction, refresh: boolean = false, errorCallback: CallableFunction = () => {}) {
@ -343,7 +343,13 @@ export class ServerManager {
this.PUT(data, callback);
}
cloneUnits(units: { ID: number; location: LatLng }[], deleteOriginal: boolean, spawnPoints: number, coalition: Coalition, callback: CallableFunction = () => {}) {
cloneUnits(
units: { ID: number; location: LatLng }[],
deleteOriginal: boolean,
spawnPoints: number,
coalition: Coalition,
callback: CallableFunction = () => {}
) {
var command = {
units: units,
coalition: coalition,
@ -589,7 +595,7 @@ export class ServerManager {
targetingRange: targetingRange,
aimMethodRange: aimMethodRange,
acquisitionRange: acquisitionRange,
}
};
var data = { setEngagementProperties: command };
this.PUT(data, callback);
@ -625,11 +631,16 @@ export class ServerManager {
loadEnvResources() {
/* Load the drawings */
this.getDrawings((drawingsData: { drawings: Record<string, Record<string, any>> }) => {
if (drawingsData) {
getApp().getDrawingsManager()?.initDrawings(drawingsData);
}
}, () => {});
this.getDrawings(
(drawingsData: { drawings: Record<string, Record<string, any>> }) => {
if (drawingsData) {
getApp().getDrawingsManager()?.initDrawings(drawingsData);
}
},
() => {}
);
// TODO: load navPoints
}
startUpdate() {
@ -795,10 +806,12 @@ export class ServerManager {
return time;
}, true);
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, true);
return time;
}, true);
window.setInterval(() => {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, true);
return time;
}, true);
}, 500);
}
checkSessionHash(newSessionHash: string) {

View File

@ -31,6 +31,7 @@ export type MapOptions = {
hideChromeWarning: boolean;
hideSecureWarning: boolean;
showMissionDrawings: boolean;
clusterGroundUnits: boolean;
};
export type MapHiddenTypes = {

View File

@ -13,6 +13,7 @@ import {
faWifi,
faHourglass,
faInfo,
faObjectGroup,
} from "@fortawesome/free-solid-svg-icons";
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
import { OlLabelToggle } from "../components/ollabeltoggle";
@ -166,8 +167,8 @@ export function Header() {
<FaFloppyDisk className={`absolute -top-3 text-2xl`} />
<FaCheck
className={`
absolute left-[9px] top-[-6px] text-2xl text-olympus-900
`}
absolute left-[9px] top-[-6px] text-2xl text-olympus-900
`}
/>
<FaCheck className={`absolute left-3 top-0 text-green-500`} />
</div>
@ -328,6 +329,13 @@ export function Header() {
className={""}
tooltip={"Hide/show units acquisition rings"}
/>
<OlRoundStateButton
onClick={() => getApp().getMap().setOption("clusterGroundUnits", !mapOptions.clusterGroundUnits)}
checked={mapOptions.clusterGroundUnits}
icon={faObjectGroup}
className={""}
tooltip={"Enable/disable ground unit clustering"}
/>
</div>
<OlLabelToggle

View File

@ -48,6 +48,7 @@ import {
colors,
UnitState,
SPOTS_EDIT_ZOOM_TRANSITION,
CLUSTERING_ZOOM_TRANSITION,
} from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { Weapon } from "../weapon/weapon";
@ -191,6 +192,8 @@ export abstract class Unit extends CustomMarker {
#acquisitionRange: number = 0;
#totalAmmo: number = 0;
#previousTotalAmmo: number = 0;
#isClusterLeader: boolean = false;
#clusterUnits: Unit[] = [];
/* Inputs timers */
#debounceTimeout: number | null = null;
@ -477,8 +480,16 @@ export abstract class Unit extends CustomMarker {
});
/* Update the marker when the options change */
MapOptionsChangedEvent.on(() => {
this.#redrawMarker();
MapOptionsChangedEvent.on((mapOptions, key) => {
if (
key === undefined ||
key === "hideGroupMembers" ||
key === "clusterGroundUnits" ||
key === "showUnitLabels" ||
key === "AWACSMode" ||
key === "AWACSCoalition"
)
this.#redrawMarker();
/* Circles don't like to be updated when the map is zooming */
if (!getApp().getMap().isZooming()) this.#drawRanges();
@ -909,7 +920,7 @@ export abstract class Unit extends CustomMarker {
/* When the group leader is selected, if grouping is active, all the other group members are also selected */
if (this.getCategory() === "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION) {
if (this.#isLeader) {
if (this.#isLeader && this.getGroupMembers().length > 0) {
/* Redraw the marker in case the leader unit was replaced by a group marker, like for SAM Sites */
this.#redrawMarker();
this.getGroupMembers().forEach((unit: Unit) => unit.setSelected(selected));
@ -918,6 +929,17 @@ export abstract class Unit extends CustomMarker {
}
}
/* When the group leader is selected, if clustering is active, all the other group members are also selected */
if (this.getCategory() === "GroundUnit" && getApp().getMap().getZoom() < CLUSTERING_ZOOM_TRANSITION) {
if (this.#isClusterLeader && this.#clusterUnits.length > 0) {
/* Redraw the marker in case the leader unit was replaced by a group marker, like for SAM Sites */
this.#redrawMarker();
this.#clusterUnits.forEach((unit: Unit) => unit.setSelected(selected));
} else {
this.#updateMarker();
}
}
/* Activate the selection effects on the marker */
this.getElement()?.querySelector(`.unit`)?.toggleAttribute("data-is-selected", selected);
@ -1107,6 +1129,17 @@ export abstract class Unit extends CustomMarker {
el.append(hotgroup);
}
/* Cluster indicator */
if (iconOptions.showCluster) {
var cluster = document.createElement("div");
cluster.classList.add("unit-cluster");
cluster.classList.add(this.getCoalition());
var clusterId = document.createElement("div");
clusterId.classList.add("unit-cluster-id");
cluster.appendChild(clusterId);
el.append(cluster);
}
/* Main icon */
if (iconOptions.showUnitIcon) {
var unitIcon = document.createElement("div");
@ -1227,6 +1260,13 @@ export abstract class Unit extends CustomMarker {
!this.getSelected() &&
this.getCategory() == "GroundUnit" &&
getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION &&
(this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0))) ||
/* Hide the unit if clustering is activated, the unit is in a cluster, it is not selected, and the zoom is higher than the clustering threshold */
(getApp().getMap().getOptions().clusterGroundUnits &&
!this.#isClusterLeader &&
!this.getSelected() &&
this.getCategory() == "GroundUnit" &&
getApp().getMap().getZoom() < CLUSTERING_ZOOM_TRANSITION &&
(this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0)));
/* Force dead units to be hidden */
@ -1287,6 +1327,14 @@ export abstract class Unit extends CustomMarker {
return getApp().getUnitsManager().getUnitByID(this.#leaderID);
}
setClusterUnits(clusterUnits: Unit[]) {
this.#clusterUnits = clusterUnits;
}
setIsClusterLeader(clusterLeader: boolean) {
this.#isClusterLeader = clusterLeader;
}
canFulfillRole(roles: string | string[]) {
if (typeof roles === "string") roles = [roles];
@ -1788,12 +1836,28 @@ export abstract class Unit extends CustomMarker {
if (hasFox3 != newHasFox3) element.querySelector(".unit")?.toggleAttribute("data-has-fox-3", newHasFox3);
if (hasOtherAmmo != newHasOtherAmmo) element.querySelector(".unit")?.toggleAttribute("data-has-other-ammo", newHasOtherAmmo);
/* Draw the hotgroup element */
element.querySelector(".unit")?.toggleAttribute("data-is-in-hotgroup", this.#hotgroup != null);
if (this.#hotgroup) {
const hotgroupEl = element.querySelector(".unit-hotgroup-id") as HTMLElement;
if (hotgroupEl) hotgroupEl.innerText = String(this.#hotgroup);
}
/* Draw the hotgroup element */
element.querySelector(".unit")?.toggleAttribute("data-is-in-hotgroup", this.#hotgroup != null);
if (this.#hotgroup) {
const hotgroupEl = element.querySelector(".unit-hotgroup-id") as HTMLElement;
if (hotgroupEl) hotgroupEl.innerText = String(this.#hotgroup);
}
/* Draw the cluster element */
element
.querySelector(".unit")
?.toggleAttribute(
"data-is-cluster-leader",
this.#isClusterLeader &&
this.#clusterUnits.length > 1 &&
getApp().getMap().getOptions().clusterGroundUnits &&
getApp().getMap().getZoom() < CLUSTERING_ZOOM_TRANSITION &&
!this.getSelected()
);
if (this.#isClusterLeader && this.#clusterUnits.length > 1) {
const clusterEl = element.querySelector(".unit-cluster-id") as HTMLElement;
if (clusterEl) clusterEl.innerText = String(this.#clusterUnits.length);
}
/* Set bullseyes positions */
const bullseyes = getApp().getMissionManager().getBullseyes();
@ -2361,6 +2425,7 @@ export abstract class AirUnit extends Unit {
showSummary: belongsToCommandedCoalition || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)),
showCallsign: belongsToCommandedCoalition && /*TODO !getApp().getMap().getOptions().AWACSMode || */ this.getHuman(),
rotateToHeading: false,
showCluster: false,
} as ObjectIconOptions;
}
@ -2448,6 +2513,7 @@ export class GroundUnit extends Unit {
showSummary: false,
showCallsign: belongsToCommandedCoalition && /*TODO !getApp().getMap().getOptions().AWACSMode || */ this.getHuman(),
rotateToHeading: false,
showCluster: true,
} as ObjectIconOptions;
}
@ -2476,6 +2542,7 @@ export class GroundUnit extends Unit {
checkZoomRedraw(): boolean {
return (
this.getIsLeader() &&
this.getGroupMembers().length > 0 &&
(getApp().getMap().getOptions().hideGroupMembers as boolean) &&
((getApp().getMap().getZoom() >= GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() < GROUPING_ZOOM_TRANSITION) ||
(getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() >= GROUPING_ZOOM_TRANSITION))
@ -2513,6 +2580,7 @@ export class NavyUnit extends Unit {
showSummary: false,
showCallsign: belongsToCommandedCoalition && /*TODO !getApp().getMap().getOptions().AWACSMode || */ this.getHuman(),
rotateToHeading: false,
showCluster: false,
} as ObjectIconOptions;
}

View File

@ -1,6 +1,6 @@
import { LatLng, LatLngBounds } from "leaflet";
import { DomEvent, DomUtil, LatLng, LatLngBounds } from "leaflet";
import { getApp } from "../olympusapp";
import { AirUnit, Unit } from "./unit";
import { AirUnit, GroundUnit, NavyUnit, Unit } from "./unit";
import {
areaContains,
bearingAndDistanceToLatLng,
@ -13,7 +13,17 @@ import {
msToKnots,
} from "../other/utils";
import { CoalitionPolygon } from "../map/coalitionarea/coalitionpolygon";
import { BLUE_COMMANDER, DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, OlympusState, RED_COMMANDER, UnitControlSubState, alarmStates } from "../constants/constants";
import {
BLUE_COMMANDER,
DELETE_CYCLE_TIME,
DELETE_SLOW_THRESHOLD,
DataIndexes,
GAME_MASTER,
IADSDensities,
OlympusState,
RED_COMMANDER,
UnitControlSubState, alarmStates,
} from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { citiesDatabase } from "./databases/citiesdatabase";
import { TemporaryUnitMarker } from "../map/markers/temporaryunitmarker";
@ -40,6 +50,7 @@ import { UnitDatabase } from "./databases/unitdatabase";
import * as turf from "@turf/turf";
import { PathMarker } from "../map/markers/pathmarker";
import { Coalition } from "../types/types";
import { ClusterMarker } from "../map/markers/clustermarker";
/** 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
@ -354,8 +365,51 @@ export class UnitsManager {
});
}
/* Compute the base clusters */
this.#clusters = this.computeClusters();
/* Compute the base air unit clusters */
this.#clusters = this.computeClusters(AirUnit);
/* Compute the base ground unit clusters */
Object.values(this.#units).forEach((unit: Unit) => unit.setIsClusterLeader(true));
if (getApp().getMap().getOptions().clusterGroundUnits) {
/* Get a list of all existing ground unit types */
let groundUnitTypes: string[] = [];
Object.values(this.#units)
.filter((unit) => unit.getAlive())
.forEach((unit: Unit) => {
if (unit.getCategory() === "GroundUnit" && !groundUnitTypes.includes(unit.getType())) groundUnitTypes.push(unit.getType());
});
["blue", "red", "neutral"].forEach((coalition: string) => {
groundUnitTypes.forEach((type: string) => {
let clusters = this.computeClusters(
GroundUnit,
(unit: Unit) => {
if (getApp().getMap().getOptions().hideGroupMembers) return unit.getType() === type && unit.getIsLeader();
else return unit.getType() === type;
},
2,
coalition as Coalition,
5
);
/* Find the unit closest to the cluster center */
Object.values(clusters).forEach((clusterUnits: Unit[]) => {
const clusterCenter = turf.center(
turf.featureCollection(clusterUnits.map((unit: Unit) => turf.point([unit.getPosition().lng, unit.getPosition().lat])))
);
const clusterCenterCoords = clusterCenter.geometry.coordinates;
const clusterCenterLatLng = new LatLng(clusterCenterCoords[1], clusterCenterCoords[0]);
const closestUnit = clusterUnits.reduce((prev, current) => {
return prev.getPosition().distanceTo(clusterCenterLatLng) < current.getPosition().distanceTo(clusterCenterLatLng) ? prev : current;
});
clusterUnits.forEach((unit: Unit) => unit.setIsClusterLeader(unit === closestUnit));
closestUnit.setClusterUnits(clusterUnits);
});
});
});
}
if (fullUpdate) UnitsRefreshedEvent.dispatch(Object.values(this.#units));
else UnitsUpdatedEvent.dispatch(updatedUnits);
@ -1294,7 +1348,9 @@ export class UnitsManager {
if (getApp().getMissionManager().getCommandModeOptions().commandMode === BLUE_COMMANDER) coalition = "blue";
else if (getApp().getMissionManager().getCommandModeOptions().commandMode === RED_COMMANDER) coalition = "red";
getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */, coalition as Coalition);
getApp()
.getServerManager()
.cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */, coalition as Coalition);
this.#showActionMessage(units, `created a group`);
} else {
getApp().addInfoMessage(`Groups can only be created from units of the same category`);
@ -1507,13 +1563,19 @@ export class UnitsManager {
getApp()
.getServerManager()
.cloneUnits(units, false, getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: spawnPoints, coalition as Coalition, (res: any) => {
if (res !== undefined) {
markers.forEach((marker: TemporaryUnitMarker) => {
marker.setCommandHash(res);
});
.cloneUnits(
units,
false,
getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER ? 0 : spawnPoints,
coalition as Coalition,
(res: any) => {
if (res !== undefined) {
markers.forEach((marker: TemporaryUnitMarker) => {
marker.setCommandHash(res);
});
}
}
});
);
}
getApp().addInfoMessage(`${this.#copiedUnits.length} units pasted`);
} else {
@ -1691,36 +1753,48 @@ export class UnitsManager {
getApp().addInfoMessage("Aircrafts can be air spawned during the SETUP phase only");
return false;
}
spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => {
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
}, 0);
spawnPoints =
getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER
? 0
: units.reduce((points: number, unit: UnitSpawnTable) => {
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
}, 0);
spawnFunction = () => getApp().getServerManager().spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints, callback);
} else if (category === "helicopter") {
if (airbase == "" && spawnsRestricted) {
getApp().addInfoMessage("Helicopters can be air spawned during the SETUP phase only");
return false;
}
spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => {
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
}, 0);
spawnPoints =
getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER
? 0
: units.reduce((points: number, unit: UnitSpawnTable) => {
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
}, 0);
spawnFunction = () => getApp().getServerManager().spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints, callback);
} else if (category === "groundunit") {
if (spawnsRestricted) {
getApp().addInfoMessage("Ground units can be spawned during the SETUP phase only");
return false;
}
spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => {
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
}, 0);
spawnPoints =
getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER
? 0
: units.reduce((points: number, unit: UnitSpawnTable) => {
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
}, 0);
spawnFunction = () => getApp().getServerManager().spawnGroundUnits(units, coalition, country, immediate, spawnPoints, callback);
} else if (category === "navyunit") {
if (spawnsRestricted) {
getApp().addInfoMessage("Navy units can be spawned during the SETUP phase only");
return false;
}
spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => {
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
}, 0);
spawnPoints =
getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER
? 0
: units.reduce((points: number, unit: UnitSpawnTable) => {
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
}, 0);
spawnFunction = () => getApp().getServerManager().spawnNavyUnits(units, coalition, country, immediate, spawnPoints, callback);
}
@ -1751,20 +1825,32 @@ export class UnitsManager {
return this.#AWACSReference;
}
computeClusters(filter: (unit: Unit) => boolean = (unit) => true, distance: number = 5 /* km */) {
computeClusters(
unitType: typeof AirUnit | typeof GroundUnit | typeof NavyUnit,
filter: (unit: Unit) => boolean = (unit) => true,
distance: number = 5 /* km */,
coalition?: Coalition,
minPoints?: number
) {
let units = Object.values(this.#units)
.filter((unit) => unit.getAlive() && unit instanceof AirUnit)
.filter((unit) => unit.getAlive() && unit instanceof unitType)
.filter(filter);
if (coalition !== undefined) {
units = units.filter((unit) => unit.getCoalition() === coalition);
}
var geojson = turf.featureCollection(units.map((unit) => turf.point([unit.getPosition().lng, unit.getPosition().lat])));
//@ts-ignore
var clustered = turf.clustersDbscan(geojson, distance, { minPoints: 1 });
var clustered = turf.clustersDbscan(geojson, distance, { minPoints: minPoints ?? 1 });
let clusters: { [key: number]: Unit[] } = {};
clustered.features.forEach((feature, idx) => {
if (clusters[feature.properties.cluster] === undefined) clusters[feature.properties.cluster] = [] as Unit[];
clusters[feature.properties.cluster].push(units[idx]);
if (feature.properties.cluster !== undefined) {
if (clusters[feature.properties.cluster] === undefined) clusters[feature.properties.cluster] = [] as Unit[];
clusters[feature.properties.cluster].push(units[idx]);
}
});
return clusters;

View File

@ -1,6 +1,7 @@
call npm run tsc
echo D|xcopy /Y /S /E .\public ..\..\build\frontend\public
echo D|xcopy /Y /S /E .\databases ..\..\build\frontend\public\databases
echo D|xcopy /Y /S /E .\views ..\..\build\frontend\cert
echo D|xcopy /Y /S /E .\build ..\..\build\frontend\build