Completed first iteration of drawings management on v2

This commit is contained in:
Davide Passoni 2025-02-26 09:39:30 +01:00
parent 1a93ee68d0
commit b2477112b1
17 changed files with 1041 additions and 17 deletions

View File

@ -22,6 +22,7 @@ Scheduler* scheduler = nullptr;
/* Data jsons */
json::value missionData = json::value::object();
json::value drawingsByLayer = json::value::object();
mutex mutexLock;
string sessionHash;
@ -161,3 +162,17 @@ extern "C" DllExport int coreMissionData(lua_State * L)
return(0);
}
extern "C" DllExport int coreDrawingsData(lua_State* L)
{
log("Olympus coreDrawingsData called successfully");
/* Lock for thread safety */
lock_guard<mutex> guard(mutexLock);
lua_getglobal(L, "Olympus");
lua_getfield(L, -1, "drawingsByLayer");
luaTableToJSON(L, -1, drawingsByLayer);
return(0);
}

View File

@ -17,6 +17,7 @@ extern UnitsManager* unitsManager;
extern WeaponsManager* weaponsManager;
extern Scheduler* scheduler;
extern json::value missionData;
extern json::value drawingsByLayer;
extern mutex mutexLock;
extern string sessionHash;
extern string instancePath;
@ -149,6 +150,11 @@ void Server::handle_get(http_request request)
else if (URI.compare(COMMANDS_URI) == 0 && query.find(L"commandHash") != query.end()) {
answer[L"commandExecuted"] = json::value(scheduler->isCommandExecuted(to_string(query[L"commandHash"])));
}
/* Drawings data*/
else if (URI.compare(DRAWINGS_URI) == 0 && drawingsByLayer.has_object_field(L"drawings")) {
log("Trying to answer with drawings...");
answer[L"drawings"] = drawingsByLayer[L"drawings"];
}
/* Common data */
answer[L"time"] = json::value::string(to_wstring(ms.count()));

View File

@ -11,12 +11,14 @@ typedef int(__stdcall* f_coreFrame)(lua_State* L);
typedef int(__stdcall* f_coreUnitsData)(lua_State* L);
typedef int(__stdcall* f_coreWeaponsData)(lua_State* L);
typedef int(__stdcall* f_coreMissionData)(lua_State* L);
typedef int(__stdcall* f_coreDrawingsData)(lua_State* L);
f_coreInit coreInit = nullptr;
f_coreDeinit coreDeinit = nullptr;
f_coreFrame coreFrame = nullptr;
f_coreUnitsData coreUnitsData = nullptr;
f_coreWeaponsData coreWeaponsData = nullptr;
f_coreMissionData coreMissionData = nullptr;
f_coreDrawingsData coreDrawingsData = nullptr;
string modPath;
@ -108,6 +110,13 @@ static int onSimulationStart(lua_State* L)
goto error;
}
coreDrawingsData = (f_coreDrawingsData)GetProcAddress(hGetProcIDDLL, "coreDrawingsData");
if (!coreDrawingsData)
{
LogError(L, "Error getting coreDrawingsData ProcAddress from DLL");
goto error;
}
coreInit(L, modPath.c_str());
LogInfo(L, "Module loaded and started successfully.");
@ -155,6 +164,8 @@ static int onSimulationStop(lua_State* L)
coreUnitsData = nullptr;
coreWeaponsData = nullptr;
coreMissionData = nullptr;
coreDrawingsData = nullptr;
}
hGetProcIDDLL = NULL;
@ -193,6 +204,15 @@ static int setMissionData(lua_State* L)
return 0;
}
static int setDrawingsData(lua_State* L)
{
if (coreDrawingsData)
{
coreDrawingsData(L);
}
return 0;
}
static const luaL_Reg Map[] = {
{"onSimulationStart", onSimulationStart},
{"onSimulationFrame", onSimulationFrame},
@ -200,6 +220,7 @@ static const luaL_Reg Map[] = {
{"setUnitsData", setUnitsData },
{"setWeaponsData", setWeaponsData },
{"setMissionData", setMissionData },
{"setDrawingsData", setDrawingsData },
{NULL, NULL}
};

View File

@ -13,6 +13,7 @@
#define SPOTS_URI "spots"
#define MISSION_URI "mission"
#define COMMANDS_URI "commands"
#define DRAWINGS_URI "drawings"
#define FRAMERATE_TIME_INTERVAL 0.05

View File

@ -42,6 +42,7 @@ export const BULLSEYE_URI = "bullseyes";
export const SPOTS_URI = "spots";
export const MISSION_URI = "mission";
export const COMMANDS_URI = "commands";
export const DRAWINGS_URI = "drawings";
export const NONE = "None";
export const GAME_MASTER = "Game master";
@ -413,6 +414,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
AWACSCoalition: "blue",
hideChromeWarning: false,
hideSecureWarning: false,
showMissionDrawings: false
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {

View File

@ -628,6 +628,25 @@ export class AWACSReferenceChangedEvent {
}
}
export class DrawingsInitEvent {
static on(callback: (drawingsData: any /*TODO*/) => void, singleShot = false) {
document.addEventListener(
this.name,
(ev: CustomEventInit) => {
callback(ev.detail);
},
{ once: singleShot }
);
}
static dispatch(drawingsData: any /*TODO*/) {
document.dispatchEvent(new CustomEvent(this.name, {detail: drawingsData}));
console.log(`Event ${this.name} dispatched`);
}
}
export class DrawingsUpdatedEvent extends BaseOlympusEvent {}
/************** Command mode events ***************/
export class CommandModeOptionsChangedEvent {
static on(callback: (options: CommandModeOptions) => void, singleShot = false) {

View File

@ -53,6 +53,7 @@ export interface SessionData {
)[];
hotgroups?: {[key: string]: number[]},
starredSpawns?: { [key: number]: SpawnRequestTable }
drawings?: { [key: string]: {visibility: boolean, opacity: number, name: string, guid: string, containers: any, drawings: any} }
}
export interface ProfileOptions {
@ -357,3 +358,41 @@ export interface ServerStatus {
connected: boolean;
paused: boolean;
}
export type DrawingPoint = {
x: number;
y: number;
};
export type PolygonPoints = DrawingPoint[] | DrawingPoint;
export type DrawingPrimitiveType = "TextBox" | "Polygon" | "Line" | "Icon";
export interface Drawing {
name: string;
visible: boolean;
mapX: number;
mapY: number;
layerName: string;
layer: string;
primitiveType: DrawingPrimitiveType;
colorString: string;
fillColorString?: string;
borderThickness?: number;
fontSize?: number;
font?: string;
text?: string;
angle?: number;
radius?: number;
points?: PolygonPoints;
style?: string;
polygonMode?: string;
thickness?: number;
width?: number;
height?: number;
closed?: boolean;
lineMode?: string;
hiddenOnPlanner?: boolean;
file?: string;
scale?: number;
}

View File

@ -0,0 +1,650 @@
import { decimalToRGBA } from "../../other/utils";
import { getApp } from "../../olympusapp";
import { DrawingsInitEvent, DrawingsUpdatedEvent, MapOptionsChangedEvent, SessionDataLoadedEvent } from "../../events";
import { MapOptions } from "../../types/types";
import { Circle, DivIcon, Layer, LayerGroup, layerGroup, Marker, Polygon, Polyline } from "leaflet";
export abstract class DCSDrawing {
#name: string;
#parent: DCSDrawingsContainer;
constructor(drawingData, parent: DCSDrawingsContainer) {
this.#name = drawingData["name"];
this.#parent = parent;
}
getName() {
return this.#name;
}
getParent() {
return this.#parent;
}
toJSON() {
return {
name: this.#name,
opacity: this.getOpacity(),
visibility: this.getVisibility(),
};
}
abstract getLayer(): Layer;
abstract setOpacity(opacity: number): void;
abstract getOpacity(): number;
abstract setVisibility(visibility: boolean): void;
abstract getVisibility(): boolean;
}
export class DCSEmptyLayer extends DCSDrawing {
getLayer() {
return layerGroup();
}
setOpacity(opacity: number): void {
//Do nothing
}
setVisibility(visibility: boolean): void {
//Do nothing
}
getOpacity(): number {
return 1;
}
getVisibility(): boolean {
return false;
}
}
export class DCSPolygon extends DCSDrawing {
#polygon: Polygon | Circle;
constructor(drawingData, parent) {
super(drawingData, parent);
const polygonMode = drawingData["polygonMode"];
let dashArray: number[] | string = [];
switch (drawingData.style) {
case "dash":
dashArray = [5];
case "dot":
dashArray = [2];
case "dotdash":
dashArray = "2, 5, 5, 5";
}
switch (polygonMode) {
case "circle":
// Example circle:
/*
colorString: 4278190335
fillColorString: 4278190127
lat: 27.65469131156049
layer: "Blue"
layerName: "Blue"
lng: 54.33075915954884
name: "SA11-2 + SA6-3"
points: {0: {}, x: 166867.07767244, y: -187576.93134045}
polygonMode: "circle"
primitiveType: "Polygon"
radius: 36651.296128911
style: "dash"
thickness: 16
visible: true*/
this.#polygon = new Circle([drawingData.lat, drawingData.lng], {
radius: Math.round(drawingData.radius),
color: `${decimalToRGBA(drawingData.colorString)}`,
fillColor: `${decimalToRGBA(drawingData.fillColorString)}`,
opacity: 1,
fillOpacity: 1,
weight: 1,
dashArray: dashArray,
});
break;
case "arrow":
const arrowBounds = [
[drawingData.points["1"].lat, drawingData.points["1"].lng],
[drawingData.points["2"].lat, drawingData.points["2"].lng],
[drawingData.points["3"].lat, drawingData.points["3"].lng],
[drawingData.points["4"].lat, drawingData.points["4"].lng],
[drawingData.points["5"].lat, drawingData.points["5"].lng],
[drawingData.points["6"].lat, drawingData.points["6"].lng],
[drawingData.points["7"].lat, drawingData.points["7"].lng],
[drawingData.points["8"].lat, drawingData.points["8"].lng],
];
this.#polygon = new Polygon(arrowBounds, {
color: `${decimalToRGBA(drawingData.colorString)}`,
fillColor: `${decimalToRGBA(drawingData.fillColorString)}`,
opacity: 1,
fillOpacity: 1,
weight: 1,
dashArray,
});
break;
case "rect":
/** Rectangle Example:
* {
"angle": 68.579040048342,
"colorString": 255,
"fillColorString": 4294901888,
"height": 11100,
"lat": 27.5547706075188,
"layer": "Author",
"layerName": "Author",
"lng": 57.22438242806247,
"mapX": 152970.68262179,
"mapY": 97907.892121675,
"name": "FLOT BUFFER EAST",
"points": {
"1": {
"lat": 27.417344649833286,
"lng": 57.34472624501578
},
"2": {
"lat": 27.38096510320196,
"lng": 57.24010993680159
},
"3": {
"lat": 27.69209116201148,
"lng": 57.1037392116416
},
"4": {
"lat": 27.728570135811577,
"lng": 57.20860735951096
}
},
"polygonMode": "rect",
"primitiveType": "Polygon",
"style": "dot",
"thickness": 16,
"visible": true,
"width": 37000
}
*/
const bounds = [
[drawingData.points["1"].lat, drawingData.points["1"].lng],
[drawingData.points["2"].lat, drawingData.points["2"].lng],
[drawingData.points["3"].lat, drawingData.points["3"].lng],
[drawingData.points["4"].lat, drawingData.points["4"].lng],
];
this.#polygon = new Polygon(bounds, {
color: `${decimalToRGBA(drawingData.colorString)}`,
fillColor: `${decimalToRGBA(drawingData.fillColorString)}`,
opacity: 1,
fillOpacity: 1,
weight: 1,
dashArray: dashArray,
});
break;
case "oval":
/**
* Example:
* {
"angle": 270,
"colorString": 255,
"fillColorString": 4278190080,
"lat": 25.032272009407105,
"layer": "Blue",
"layerName": "Blue",
"lng": 55.36597899137401,
"mapX": -125416.92956726,
"mapY": -89103.936896595,
"name": "AM OTP",
"points": {
"1": {
"lat": 25.03332743167039,
"lng": 55.34689257576858
},
"2": {
"lat": 25.034529398092356,
"lng": 55.348849087588164
},
...
"24": {
"lat": 25.032053589358366,
"lng": 55.34623694782629
}
},
"polygonMode": "oval",
"primitiveType": "Polygon",
"r1": 1992.4714734497,
"r2": 541.99904672895,
"style": "dot",
"thickness": 10,
"visible": true
}
*/
const points: [number, number][] = Object.values(drawingData.points as Record<string, { lat: number; lng: number }>).map((p) => [p.lat, p.lng]);
this.#polygon = new Polygon(points, {
color: `${decimalToRGBA(drawingData.colorString)}`,
fillColor: `${decimalToRGBA(drawingData.fillColorString)}`,
opacity: 1,
fillOpacity: 1,
weight: drawingData.thickness,
dashArray: dashArray,
});
case "free":
const freePolypoints: [number, number][] = Object.values(drawingData.points as Record<string, { lat: number; lng: number }>).map((p) => [p.lat, p.lng]);
this.#polygon = new Polygon(freePolypoints, {
color: `${decimalToRGBA(drawingData.colorString)}`,
fillColor: `${decimalToRGBA(drawingData.fillColorString)}`,
opacity: 1,
fillOpacity: 1,
weight: drawingData.thickness,
dashArray: dashArray,
});
break;
default:
break;
}
this.setVisibility(true);
}
getLayer() {
return this.#polygon;
}
setOpacity(opacity: number): void {
if (opacity === this.#polygon.options.fillOpacity && opacity === this.#polygon.options.opacity) return;
this.#polygon.options.fillOpacity = opacity;
this.#polygon.options.opacity = opacity;
this.#polygon.redraw();
getApp().getDrawingsManager().requestUpdateEventDispatch();
}
setVisibility(visibility: boolean): void {
if (visibility && !this.getParent().getLayerGroup().hasLayer(this.#polygon)) this.#polygon.addTo(this.getParent().getLayerGroup());
//@ts-ignore Leaflet typings are wrong
if (!visibility && this.getParent().getLayerGroup().hasLayer(this.#polygon)) this.#polygon.removeFrom(this.getParent().getLayerGroup());
if (visibility && !this.getParent().getVisibility()) this.getParent().setVisibility(true);
getApp().getDrawingsManager().requestUpdateEventDispatch();
}
getOpacity(): number {
return this.#polygon.options.opacity ?? 1;
}
getVisibility(): boolean {
return this.getParent().getLayerGroup().hasLayer(this.#polygon);
}
}
export class DCSLine extends DCSDrawing {
#line: Polyline;
constructor(drawingData, parent) {
super(drawingData, parent);
const points: [number, number][] = Object.values(drawingData.points as Record<string, { lat: number; lng: number }>).map((p) => [p.lat, p.lng]);
const dashArray = drawingData.style === "dot" ? "5" : drawingData.style === "dot2" ? "10" : undefined;
this.#line = new Polyline(points, {
color: `${decimalToRGBA(drawingData.colorString)}`,
weight: drawingData.thickness,
dashArray: dashArray,
});
this.setVisibility(true);
}
getLayer() {
return this.#line;
}
setOpacity(opacity: number): void {
if (opacity === this.#line.options.opacity) return;
this.#line.options.opacity = opacity;
this.#line.redraw();
getApp().getDrawingsManager().requestUpdateEventDispatch();
}
setVisibility(visibility: boolean): void {
if (visibility && !this.getParent().getLayerGroup().hasLayer(this.#line)) this.#line.addTo(this.getParent().getLayerGroup());
//@ts-ignore Leaflet typings are wrong
if (!visibility && this.getParent().getLayerGroup().hasLayer(this.#line)) this.#line.removeFrom(this.getParent().getLayerGroup());
if (visibility && !this.getParent().getVisibility()) this.getParent().setVisibility(true);
getApp().getDrawingsManager().requestUpdateEventDispatch();
}
getOpacity(): number {
return this.#line.options.opacity ?? 1;
}
getVisibility(): boolean {
return this.getParent().getLayerGroup().hasLayer(this.#line);
}
}
export class DCSTextBox extends DCSDrawing {
#marker: Marker;
constructor(drawingData, parent) {
super(drawingData, parent);
/* Example textbox "ABC625":
angle: 0
borderThickness: 1
colorString: 4294967295
fillColorString: 8421504
font: "DejaVuLGCSansCondensed.ttf"
fontSize: 10
layer: "Common"
layerName: "Common"
mapX: -261708.68309463
mapY: -217863.03743212
name: "ABC625"
primitiveType: "TextBox"
text: "ABC625"
visible: true
*/
const customIcon = new DivIcon({
html: `
<div style="
border: ${drawingData.borderThickness}px solid ${decimalToRGBA(drawingData.colorString)};
background-color: ${decimalToRGBA(drawingData.fillColorString)};
color: ${decimalToRGBA(drawingData.colorString)};
padding: 5px;
font-family: Arial, sans-serif;
font-size: ${drawingData.fontSize - 1}px;
text-align: center;
width: max-content;
transform: rotate(${drawingData.angle}deg);
transform-origin: center;
opacity: 100%;
">
${drawingData.text || drawingData.name}
</div>
`,
// iconSize: [100, 50], // Dimensioni del box
iconAnchor: [50, 25], // Punto di ancoraggio al centro
className: "",
});
this.#marker = new Marker([drawingData.lat, drawingData.lng], { icon: customIcon });
this.setVisibility(true);
}
getLayer() {
return this.#marker;
}
setOpacity(opacity: number): void {
if (opacity === this.#marker.options.opacity) return;
this.#marker.options.opacity = opacity;
/* Hack to force marker redraw */
const originalVisibility = this.getVisibility();
this.setVisibility(false);
this.setVisibility(originalVisibility);
getApp().getDrawingsManager().requestUpdateEventDispatch();
}
setVisibility(visibility: boolean): void {
if (visibility && !this.getParent().getLayerGroup().hasLayer(this.#marker)) this.#marker.addTo(this.getParent().getLayerGroup());
//@ts-ignore Leaflet typings are wrong
if (!visibility && this.getParent().getLayerGroup().hasLayer(this.#marker)) this.#marker.removeFrom(this.getParent().getLayerGroup());
if (visibility && !this.getParent().getVisibility()) this.getParent().setVisibility(true);
getApp().getDrawingsManager().requestUpdateEventDispatch();
}
getOpacity(): number {
return this.#marker.options.opacity ?? 1;
}
getVisibility(): boolean {
return this.getParent().getLayerGroup().hasLayer(this.#marker);
}
}
export class DCSDrawingsContainer {
#drawings: DCSDrawing[] = [];
#subContainers: DCSDrawingsContainer[] = [];
#name: string;
#opacity: number = 1;
#visibility: boolean = true;
#layerGroup: LayerGroup = new LayerGroup();
#parent: LayerGroup | DCSDrawingsContainer;
#guid: string = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
constructor(name: string, parent: LayerGroup | DCSDrawingsContainer) {
this.#name = name;
this.#parent = parent;
this.#layerGroup.addTo(parent instanceof DCSDrawingsContainer ? parent.getLayerGroup() : parent);
}
getGuid() {
return this.#guid;
}
initFromData(drawingsData) {
let hasContainers = false;
Object.keys(drawingsData).forEach((layerName: string) => {
if (drawingsData[layerName]["name"] === undefined) {
const newContainer = new DCSDrawingsContainer(layerName, this);
this.addSubContainer(newContainer);
newContainer.initFromData(drawingsData[layerName]);
hasContainers = true;
}
});
const othersContainer = new DCSDrawingsContainer("Others", this);
if (hasContainers) this.addSubContainer(othersContainer);
Object.keys(drawingsData).forEach((layerName: string) => {
const primitiveType = drawingsData[layerName]["primitiveType"];
// Possible primitives:
// "Line","TextBox","Polygon","Icon"
// Possible polygon modes:
// "arrow","circle","rect","oval","free"
// Possible Line modes:
// 'segments', 'free', 'segment'
let newDrawing = new DCSEmptyLayer(drawingsData[layerName], othersContainer) as DCSDrawing;
switch (primitiveType) {
case "Polygon":
newDrawing = new DCSPolygon(drawingsData[layerName], othersContainer);
break;
case "TextBox":
newDrawing = new DCSTextBox(drawingsData[layerName], othersContainer);
break;
case "Line":
newDrawing = new DCSLine(drawingsData[layerName], othersContainer);
break;
case "Icon":
break;
default:
break;
}
if (hasContainers) othersContainer.addDrawing(newDrawing);
else this.addDrawing(newDrawing);
});
if (othersContainer.getDrawings().length === 0) this.removeSubContainer(othersContainer); // Remove empty container
}
getLayerGroup() {
return this.#layerGroup;
}
getName() {
return this.#name;
}
addDrawing(drawing: DCSDrawing) {
this.#drawings.push(drawing);
}
addSubContainer(container: DCSDrawingsContainer) {
this.#subContainers.push(container);
}
removeSubContainer(container: DCSDrawingsContainer) {
const index = this.#subContainers.indexOf(container);
if (index !== -1) this.#subContainers.splice(index, 1);
}
getDrawings() {
return this.#drawings;
}
getSubContainers() {
return this.#subContainers;
}
setOpacity(opacity: number) {
if (opacity === this.#opacity) return;
this.#opacity = opacity;
this.#drawings.forEach((drawing) => drawing.setOpacity(opacity));
this.#subContainers.forEach((container) => container.setOpacity(opacity));
getApp().getDrawingsManager().requestUpdateEventDispatch();
}
setVisibility(visibility: boolean, force: boolean = false) {
this.#visibility = visibility;
if (force) {
this.#subContainers.forEach((container) => container.setVisibility(visibility, force));
this.#drawings.forEach((drawing) => drawing.setVisibility(visibility));
}
if (visibility && this.#parent instanceof DCSDrawingsContainer && !this.#parent.getVisibility()) this.#parent.setVisibility(true);
if (this.#parent instanceof DCSDrawingsContainer) {
if (visibility && !this.#parent.getLayerGroup().hasLayer(this.#layerGroup)) this.#layerGroup.addTo(this.#parent.getLayerGroup());
//@ts-ignore Leaflet typings are wrong
if (!visibility && this.#parent.getLayerGroup().hasLayer(this.#layerGroup)) this.#layerGroup.removeFrom(this.#parent.getLayerGroup());
} else {
if (visibility && !this.#parent.hasLayer(this.#layerGroup)) this.#layerGroup.addTo(this.#parent);
//@ts-ignore Leaflet typings are wrong
if (!visibility && this.#parent.hasLayer(this.#layerGroup)) this.#layerGroup.removeFrom(this.#parent);
}
getApp().getDrawingsManager().requestUpdateEventDispatch();
}
getOpacity() {
return this.#opacity;
}
getVisibility() {
return this.#visibility;
}
toJSON() {
let JSON = { drawings: {}, containers: {}, guid: this.#guid, name: this.#name, opacity: this.#opacity, visibility: this.#visibility };
this.#drawings.forEach((drawing) => {
JSON["drawings"][drawing.getName()] = drawing.toJSON();
});
this.#subContainers.forEach((container) => {
JSON["containers"][container.getName()] = container.toJSON();
});
return JSON;
}
fromJSON(JSON) {
this.#subContainers.forEach((container) => {
if (JSON["containers"][container.getName()]) container.fromJSON(JSON["containers"][container.getName()]);
});
this.#drawings.forEach((drawing) => {
if (JSON["drawings"][drawing.getName()]) {
drawing.setOpacity(JSON["drawings"][drawing.getName()].opacity);
drawing.setVisibility(JSON["drawings"][drawing.getName()].visibility);
}
});
this.setOpacity(JSON["opacity"]);
this.setVisibility(JSON["visibility"]);
}
hasSearchString(searchString) {
if (this.getName().toLowerCase().includes(searchString.toLowerCase())) return true;
if (
this.getDrawings().some((drawing) =>
drawing
.getName()
?.toLowerCase()
.includes(searchString.toLowerCase() ?? false)
)
)
return true;
if (this.getSubContainers().some((container) => container.hasSearchString(searchString))) return true;
return false;
}
}
export class DrawingsManager {
#drawingsContainer: DCSDrawingsContainer;
#updateEventRequested: boolean = false;
#sessionDataDrawings = {};
#initialized: boolean = false;
constructor() {
const drawingsLayerGroup = new LayerGroup();
drawingsLayerGroup.addTo(getApp().getMap());
this.#drawingsContainer = new DCSDrawingsContainer("Mission drawings", drawingsLayerGroup);
MapOptionsChangedEvent.on((mapOptions: MapOptions) => {
this.#drawingsContainer.setVisibility(mapOptions.showMissionDrawings);
});
SessionDataLoadedEvent.on((sessionData) => {
this.#sessionDataDrawings = sessionData.drawings ?? {};
if (this.#initialized) if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]);
});
}
initDrawings(data: { drawings: Record<string, Record<string, any>> }): boolean {
if (data && data.drawings) {
this.#drawingsContainer.initFromData(data.drawings);
if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]);
DrawingsInitEvent.dispatch(this.#drawingsContainer);
this.#initialized = true;
return true;
} else {
console.error("Error while initializing drawings, empty data");
return false;
}
}
getDrawingsContainer() {
return this.#drawingsContainer;
}
requestUpdateEventDispatch() {
if (this.#updateEventRequested) return;
this.#updateEventRequested = true;
window.setTimeout(() => {
this.#updateEventRequested = false;
DrawingsUpdatedEvent.dispatch();
}, 100);
}
}

View File

@ -48,6 +48,7 @@ import {
ConfigLoadedEvent,
ContextActionChangedEvent,
ContextActionSetChangedEvent,
DrawingsInitEvent,
HiddenTypesChangedEvent,
MapContextMenuRequestEvent,
MapOptionsChangedEvent,
@ -344,7 +345,8 @@ export class Map extends L.Map {
shiftKey: false,
altKey: false,
ctrlKey: false,
}).addShortcut("clearMeasures", {
})
.addShortcut("clearMeasures", {
label: "Clear measures",
keyUpCallback: () => {
this.clearMeasures();
@ -1329,4 +1331,12 @@ export class Map extends L.Map {
this.#measures.forEach((measure) => measure.remove());
this.#measures = [];
}
getMapLayer(layerName: string) {
return this.#mapLayers[layerName] && this.#mapLayers[layerName];
}
getAllMapLayers() {
return this.#mapLayers;
}
}

View File

@ -27,6 +27,7 @@ import { SessionDataManager } from "./sessiondata";
import { ControllerManager } from "./controllers/controllermanager";
import { AWACSController } from "./controllers/awacs";
import { CoalitionAreasManager } from "./map/coalitionarea/coalitionareamanager";
import { DrawingsManager } from "./map/drawings/drawingsmanager";
export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
export var IP = window.location.toString();
@ -53,6 +54,7 @@ export class OlympusApp {
#sessionDataManager: SessionDataManager;
#controllerManager: ControllerManager;
#coalitionAreasManager: CoalitionAreasManager;
#drawingsManager: DrawingsManager;
//#pluginsManager: // TODO
constructor() {
@ -105,6 +107,10 @@ export class OlympusApp {
return this.#coalitionAreasManager;
}
getDrawingsManager() {
return this.#drawingsManager;
}
/* TODO
getPluginsManager() {
return null // this.#pluginsManager as PluginsManager;
@ -125,6 +131,7 @@ export class OlympusApp {
this.#audioManager = new AudioManager();
this.#controllerManager = new ControllerManager();
this.#coalitionAreasManager = new CoalitionAreasManager();
this.#drawingsManager = new DrawingsManager();
/* Check if we are running the latest version */
const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json");

View File

@ -645,4 +645,13 @@ export function normalizeAngle(angle: number): number {
angle += 360;
}
return angle;
}
export function decimalToRGBA(decimal: number): string {
const r = (decimal >>> 24) & 0xff;
const g = (decimal >>> 16) & 0xff;
const b = (decimal >>> 8) & 0xff;
const a = (decimal & 0xff) / 255;
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
}

View File

@ -4,6 +4,7 @@ import {
AIRBASES_URI,
BULLSEYE_URI,
COMMANDS_URI,
DRAWINGS_URI,
LOGS_URI,
MISSION_URI,
NONE,
@ -220,6 +221,10 @@ export class ServerManager {
this.GET(callback, errorCallback, WEAPONS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[WEAPONS_URI] }, "arraybuffer", refresh);
}
getDrawings(callback: CallableFunction, errorCallback: CallableFunction, refresh: boolean = false) {
this.GET(callback, errorCallback, DRAWINGS_URI);
}
isCommandExecuted(callback: CallableFunction, commandHash: string, errorCallback: CallableFunction = () => {}) {
this.GET(callback, errorCallback, COMMANDS_URI, {
commandHash: commandHash,
@ -577,6 +582,17 @@ export class ServerManager {
this.PUT(data, callback);
}
loadEnvResources() {
/* Load the drawings */
this.getDrawings((drawingsData: { drawings: Record<string, Record<string, any>> }) => {
if (drawingsData) {
getApp().getDrawingsManager()?.initDrawings(drawingsData);
}
}, () => {});
// TODO: load navPoints
}
startUpdate() {
/* Clear any existing interval */
this.#intervals.forEach((interval: number) => {
@ -584,6 +600,9 @@ export class ServerManager {
});
this.#intervals = [];
// Load mission env resources (one shot)
this.loadEnvResources();
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused()) {

View File

@ -8,6 +8,7 @@ import {
AudioSinksChangedEvent,
AudioSourcesChangedEvent,
CoalitionAreasChangedEvent,
DrawingsUpdatedEvent,
HotgroupsChangedEvent,
SessionDataChangedEvent,
SessionDataLoadedEvent,
@ -130,7 +131,13 @@ export class SessionDataManager {
StarredSpawnsChangedEvent.on((starredSpawns) => {
this.#sessionData.starredSpawns = starredSpawns;
this.#saveSessionData();
})
});
DrawingsUpdatedEvent.on(() => {
let container = getApp().getDrawingsManager().getDrawingsContainer();
this.#sessionData.drawings = {"Mission drawings": container.toJSON()};
this.#saveSessionData();
});
}, 200);
});
}

View File

@ -29,6 +29,7 @@ export type MapOptions = {
AWACSCoalition: Coalition;
hideChromeWarning: boolean;
hideSecureWarning: boolean;
showMissionDrawings: boolean;
};
export type MapHiddenTypes = {

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState } from "react";
import { Menu } from "./components/menu";
import { FaArrowDown, FaArrowUp, FaTrash } from "react-icons/fa";
import { FaArrowDown, FaArrowUp, FaChevronRight, FaTrash } from "react-icons/fa";
import { getApp } from "../../olympusapp";
import { OlStateButton } from "../components/olstatebutton";
import { faDrawPolygon } from "@fortawesome/free-solid-svg-icons";
import { faDrawPolygon, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
import { CoalitionPolygon } from "../../map/coalitionarea/coalitionpolygon";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
@ -13,9 +13,12 @@ import { Coalition } from "../../types/types";
import { OlRangeSlider } from "../components/olrangeslider";
import { CoalitionCircle } from "../../map/coalitionarea/coalitioncircle";
import { DrawSubState, ERAS_ORDER, IADSTypes, NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants";
import { AppStateChangedEvent, CoalitionAreasChangedEvent, CoalitionAreaSelectedEvent } from "../../events";
import { AppStateChangedEvent, CoalitionAreasChangedEvent, CoalitionAreaSelectedEvent, DrawingsInitEvent, DrawingsUpdatedEvent } from "../../events";
import { FaXmark } from "react-icons/fa6";
import { deepCopyTable } from "../../other/utils";
import { DCSDrawingsContainer, DCSEmptyLayer } from "../../map/drawings/drawingsmanager";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OlSearchBar } from "../components/olsearchbar";
export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
@ -30,11 +33,21 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
const [erasSelection, setErasSelection] = useState({});
const [rangesSelection, setRangesSelection] = useState({});
const [openContainers, setOpenContainers] = useState([] as DCSDrawingsContainer[]);
const [mainDrawingsContainer, setDrawingsContainer] = useState({ container: null } as { container: null | DCSDrawingsContainer });
const [searchString, setSearchString] = useState("");
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
setAppState(state);
setAppSubState(subState);
});
DrawingsInitEvent.on((drawingContainer) => {
setDrawingsContainer({ container: drawingContainer });
});
DrawingsUpdatedEvent.on(() => {
setDrawingsContainer({ container: getApp().getDrawingsManager().getDrawingsContainer() });
});
}, []);
/* Get all the unique types and eras for groundunits */
@ -50,6 +63,85 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
CoalitionAreasChangedEvent.on((coalitionAreas) => setCoalitionAreas([...coalitionAreas]));
}, []);
function renderDrawingsContainerControls(container: DCSDrawingsContainer) {
if (container.hasSearchString(searchString)) {
return (
<div className="ml-2 flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="flex justify-between gap-2">
<FaChevronRight
className={`
my-auto
${openContainers.includes(container) && `rotate-90`}
cursor-pointer text-gray-400 transition-transform
`}
onClick={() => {
if (openContainers.includes(container)) {
let index = openContainers.indexOf(container);
openContainers.splice(index, 1);
} else {
openContainers.push(container);
}
setOpenContainers([...openContainers]);
}}
></FaChevronRight>
<FontAwesomeIcon
icon={container.getVisibility() ? faEye : faEyeSlash}
className={`
my-auto w-6 cursor-pointer text-gray-400 transition-transform
hover:scale-125 hover:text-gray-200
`}
onClick={() => {
container.setVisibility(!container.getVisibility(), true);
}}
/>
<div
className={`
w-40 w-max-40 overflow-hidden text-ellipsis text-nowrap bg-
`}
>
{container.getName()}
</div>
<OlRangeSlider
value={container.getOpacity() * 100}
min={0}
max={100}
onChange={(ev) => {
container.setOpacity(Number(ev.currentTarget.value) / 100);
}}
className={`my-auto ml-auto max-w-32`}
></OlRangeSlider>
</div>
</div>
{openContainers.includes(container) && container.getSubContainers().map((container) => renderDrawingsContainerControls(container))}
{openContainers.includes(container) &&
container.getDrawings().map((drawing) => {
if (drawing instanceof DCSEmptyLayer) return <></>;
return (
<div className="ml-4 flex justify-start gap-2">
<FontAwesomeIcon
icon={drawing.getVisibility() ? faEye : faEyeSlash}
className={`
my-auto w-6 cursor-pointer text-gray-400
transition-transform
hover:scale-125 hover:text-gray-200
`}
onClick={() => {
drawing.setVisibility(!drawing.getVisibility());
}}
/>
<div className={`overflow-hidden text-ellipsis text-nowrap`}>{drawing.getName()}</div>
</div>
);
})}
</div>
);
} else {
return <></>;
}
}
return (
<Menu
open={props.open}
@ -132,6 +224,14 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
<div className="text-sm">Add circle</div>
</OlStateButton>
</div>
<div>
<div className="flex flex-col gap-2 p-6">
<div className="text-sm text-gray-400">Mission drawings</div>
<OlSearchBar onChange={(search) => setSearchString(search)} text={searchString || ""}></OlSearchBar>
<div className="flex flex-col gap-2">{mainDrawingsContainer.container && renderDrawingsContainerControls(mainDrawingsContainer.container)}</div>
</div>
</div>
</div>
)}
</>

View File

@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import { OlRoundStateButton, OlStateButton, OlLockStateButton } from "../components/olstatebutton";
import { faSkull, faCamera, faFlag, faVolumeHigh, faDownload, faUpload } from "@fortawesome/free-solid-svg-icons";
import { faSkull, faCamera, faFlag, faVolumeHigh, faDownload, faUpload, faDrawPolygon } from "@fortawesome/free-solid-svg-icons";
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
import { OlLabelToggle } from "../components/ollabeltoggle";
import { getApp, IP } from "../../olympusapp";
@ -16,8 +16,24 @@ import {
olButtonsVisibilityOlympus,
} from "../components/olicons";
import { FaChevronLeft, FaChevronRight, FaFloppyDisk } from "react-icons/fa6";
import { CommandModeOptionsChangedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent, SessionDataChangedEvent, SessionDataSavedEvent } from "../../events";
import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, ImportExportSubstate, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, OlympusState, RED_COMMANDER } from "../../constants/constants";
import {
CommandModeOptionsChangedEvent,
ConfigLoadedEvent,
HiddenTypesChangedEvent,
MapOptionsChangedEvent,
MapSourceChangedEvent,
SessionDataChangedEvent,
SessionDataSavedEvent,
} from "../../events";
import {
BLUE_COMMANDER,
COMMAND_MODE_OPTIONS_DEFAULTS,
ImportExportSubstate,
MAP_HIDDEN_TYPES_DEFAULTS,
MAP_OPTIONS_DEFAULTS,
OlympusState,
RED_COMMANDER,
} from "../../constants/constants";
import { OlympusConfig } from "../../interfaces";
import { FaCheck, FaSave, FaSpinner } from "react-icons/fa";
@ -116,15 +132,40 @@ export function Header() {
{IP}
</div>
</div>
{savingSessionData ? <div className="text-white"><FaSpinner className={`
animate-spin text-2xl
`}/></div> : <div className={`relative text-white`}><FaFloppyDisk className={`
absolute -top-3 text-2xl
`}/><FaCheck className={`
absolute left-[9px] top-[-6px] text-2xl text-olympus-900
`}/><FaCheck className={`absolute left-3 top-0 text-green-500`}/></div>}
<OlStateButton className="ml-8" icon={faDownload} onClick={() => {getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.EXPORT)}} checked={false}/>
<OlStateButton icon={faUpload} onClick={() => {getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.IMPORT)}} checked={false}/>
{savingSessionData ? (
<div className="text-white">
<FaSpinner
className={`animate-spin text-2xl`}
/>
</div>
) : (
<div className={`relative text-white`}>
<FaFloppyDisk
className={`absolute -top-3 text-2xl`}
/>
<FaCheck
className={`
absolute left-[9px] top-[-6px] text-2xl text-olympus-900
`}
/>
<FaCheck className={`absolute left-3 top-0 text-green-500`} />
</div>
)}
<OlStateButton
className="ml-8"
icon={faDownload}
onClick={() => {
getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.EXPORT);
}}
checked={false}
/>
<OlStateButton
icon={faUpload}
onClick={() => {
getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.IMPORT);
}}
checked={false}
/>
</div>
{commandModeOptions.commandMode === BLUE_COMMANDER && (
@ -138,6 +179,14 @@ export function Header() {
</div>
)}
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
<OlRoundStateButton
icon={faDrawPolygon}
checked={mapOptions.showMissionDrawings}
onClick={() => {
getApp().getMap().setOption("showMissionDrawings", !mapOptions.showMissionDrawings);
}}
tooltip="Show/Hide mission drawings"
/>
<OlLockStateButton
checked={!mapOptions.protectDCSUnits}
onClick={() => {

View File

@ -13,6 +13,7 @@ Olympus.log = mist.Logger:new("Olympus", 'info')
Olympus.missionData = {}
Olympus.unitsData = {}
Olympus.weaponsData = {}
Olympus.drawingsByLayer = {}
-- Units data structures
Olympus.unitCounter = 1 -- Counter to generate unique names
@ -1075,6 +1076,71 @@ function getUnitDescription(unit)
return unit:getDescr()
end
-- This function is periodically called to collect the data of all the existing drawings in the mission to be transmitted to the olympus.dll
function Olympus.initializeDrawings()
local drawings = {}
if mist.DBs.drawingByName ~= nil then
for drawingName, drawingData in pairs(mist.DBs.drawingByName) do
local customLayer = drawingData.name:match("^%[LYR:(.-)%]")
-- Let's convert DCS coords to lat lon
local vec3 = { x = drawingData['mapX'], y = 0, z = drawingData['mapY'] }
local lat, lng = coord.LOtoLL(vec3)
drawingData['lat'] = lat
drawingData['lng'] = lng
-- If the drawing has points, we have to convert those too
if drawingData['points'] ~= nil then
if drawingData['points']['x'] ~= nil then
-- In this case we have only one point
local point = { x = drawingData['points']['x'], y = 0, z = drawingData['points']['y'] }
local pointLat, pointLng = coord.LOtoLL(point)
drawingData['points'][0] = { lat = pointLat, lng = pointLng }
else
-- In this case we have multiple points indexed by number
for pointNumber, pointLOCoords in pairs(drawingData['points']) do
local point = { x = pointLOCoords['x'], y = 0, z = pointLOCoords['y'] }
local pointLat, pointLng = coord.LOtoLL(point)
drawingData['points'][pointNumber] = { lat = pointLat, lng = pointLng }
end
end
end
-- Let's initialize the layers
if drawings[drawingData.layerName] == nil then
drawings[drawingData.layerName] = {}
end
if customLayer and drawings[drawingData.layerName][customLayer] == nil then
drawings[drawingData.layerName][customLayer] = {}
end
-- Let's put the drawing in the correct layer
if customLayer then
-- Let's remove the tag from the drawing name
local cleanDrawingName = string.match(drawingName, "%] (.+)")
drawingData.name = cleanDrawingName
-- The drawing has the custom layer tag
drawings[drawingData.layerName][customLayer][cleanDrawingName] = drawingData
else
-- The drawing is a standard drawing
drawings[drawingData.layerName][drawingName] = drawingData
end
end
Olympus.drawingsByLayer["drawings"] = drawings
-- Send the drawings to the DLL
Olympus.OlympusDLL.setDrawingsData()
Olympus.notify("Olympus drawings initialized", 2)
else
Olympus.debug("MIST DBs not ready", 2)
timer.scheduleFunction(Olympus.initializeDrawings, {}, timer.getTime() + 1)
end
end
-- This function is periodically called to collect the data of all the existing units in the mission to be transmitted to the olympus.dll
function Olympus.setUnitsData(arg, time)
-- Units data
@ -1560,5 +1626,8 @@ timer.scheduleFunction(Olympus.setMissionData, {}, timer.getTime() + 1)
-- Initialize the ME units
Olympus.initializeUnits()
-- Initialize the Drawings
Olympus.initializeDrawings()
Olympus.notify("OlympusCommand script " .. version .. " loaded successfully", 2, true)