From b2477112b1ef8f192662c8d45206cd69b3d27f40 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Wed, 26 Feb 2025 09:39:30 +0100 Subject: [PATCH] Completed first iteration of drawings management on v2 --- backend/core/src/core.cpp | 15 + backend/core/src/server.cpp | 6 + backend/olympus/src/olympus.cpp | 21 + backend/shared/include/defines.h | 1 + frontend/react/src/constants/constants.ts | 2 + frontend/react/src/events.ts | 19 + frontend/react/src/interfaces.ts | 39 ++ .../react/src/map/drawings/drawingsmanager.ts | 650 ++++++++++++++++++ frontend/react/src/map/map.ts | 12 +- frontend/react/src/olympusapp.ts | 7 + frontend/react/src/other/utils.ts | 9 + frontend/react/src/server/servermanager.ts | 19 + frontend/react/src/sessiondata.ts | 9 +- frontend/react/src/types/types.ts | 1 + frontend/react/src/ui/panels/drawingmenu.tsx | 106 ++- frontend/react/src/ui/panels/header.tsx | 73 +- scripts/lua/backend/OlympusCommand.lua | 69 ++ 17 files changed, 1041 insertions(+), 17 deletions(-) create mode 100644 frontend/react/src/map/drawings/drawingsmanager.ts diff --git a/backend/core/src/core.cpp b/backend/core/src/core.cpp index e52c7612..c5376e56 100644 --- a/backend/core/src/core.cpp +++ b/backend/core/src/core.cpp @@ -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 guard(mutexLock); + + lua_getglobal(L, "Olympus"); + lua_getfield(L, -1, "drawingsByLayer"); + luaTableToJSON(L, -1, drawingsByLayer); + + return(0); +} \ No newline at end of file diff --git a/backend/core/src/server.cpp b/backend/core/src/server.cpp index 6fc893ca..b0f911c7 100644 --- a/backend/core/src/server.cpp +++ b/backend/core/src/server.cpp @@ -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())); diff --git a/backend/olympus/src/olympus.cpp b/backend/olympus/src/olympus.cpp index b080000f..456014f7 100644 --- a/backend/olympus/src/olympus.cpp +++ b/backend/olympus/src/olympus.cpp @@ -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} }; diff --git a/backend/shared/include/defines.h b/backend/shared/include/defines.h index 156d6c74..3280eac3 100644 --- a/backend/shared/include/defines.h +++ b/backend/shared/include/defines.h @@ -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 diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 2b55b5e5..b9e89445 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -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 = { diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index b95f20b9..cb989c05 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -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) { diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 6fe65d4c..d8f892de 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -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; +} \ No newline at end of file diff --git a/frontend/react/src/map/drawings/drawingsmanager.ts b/frontend/react/src/map/drawings/drawingsmanager.ts new file mode 100644 index 00000000..75be02a6 --- /dev/null +++ b/frontend/react/src/map/drawings/drawingsmanager.ts @@ -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).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).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).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: ` +
+ ${drawingData.text || drawingData.name} +
+ `, + // 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> }): 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); + } +} diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index a3764e8e..51a0b2ee 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -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; + } } diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index 31acd33d..4329e6e9 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -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"); diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index ca35007c..d0942f12 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -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)})`; } \ No newline at end of file diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index c6cfffb9..f80ce8ee 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -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> }) => { + 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()) { diff --git a/frontend/react/src/sessiondata.ts b/frontend/react/src/sessiondata.ts index 382a99f3..33301fb4 100644 --- a/frontend/react/src/sessiondata.ts +++ b/frontend/react/src/sessiondata.ts @@ -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); }); } diff --git a/frontend/react/src/types/types.ts b/frontend/react/src/types/types.ts index e4fed8f8..7dc22e28 100644 --- a/frontend/react/src/types/types.ts +++ b/frontend/react/src/types/types.ts @@ -29,6 +29,7 @@ export type MapOptions = { AWACSCoalition: Coalition; hideChromeWarning: boolean; hideSecureWarning: boolean; + showMissionDrawings: boolean; }; export type MapHiddenTypes = { diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index 8bf2ed41..fa75d8ea 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -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 ( +
+
+
+ { + if (openContainers.includes(container)) { + let index = openContainers.indexOf(container); + openContainers.splice(index, 1); + } else { + openContainers.push(container); + } + setOpenContainers([...openContainers]); + }} + > + { + container.setVisibility(!container.getVisibility(), true); + }} + /> +
+ {container.getName()} +
+ + { + container.setOpacity(Number(ev.currentTarget.value) / 100); + }} + className={`my-auto ml-auto max-w-32`} + > +
+
+ {openContainers.includes(container) && container.getSubContainers().map((container) => renderDrawingsContainerControls(container))} + {openContainers.includes(container) && + container.getDrawings().map((drawing) => { + if (drawing instanceof DCSEmptyLayer) return <>; + return ( +
+ { + drawing.setVisibility(!drawing.getVisibility()); + }} + /> +
{drawing.getName()}
+
+ ); + })} +
+ ); + } else { + return <>; + } + } + return ( void }) {
Add circle
+ +
+
+
Mission drawings
+ setSearchString(search)} text={searchString || ""}> +
{mainDrawingsContainer.container && renderDrawingsContainerControls(mainDrawingsContainer.container)}
+
+
)} diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 54a8acbb..827aafac 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -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} - {savingSessionData ?
:
} - {getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.EXPORT)}} checked={false}/> - {getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.IMPORT)}} checked={false}/> + {savingSessionData ? ( +
+ +
+ ) : ( +
+ + + +
+ )} + { + getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.EXPORT); + }} + checked={false} + /> + { + getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.IMPORT); + }} + checked={false} + /> {commandModeOptions.commandMode === BLUE_COMMANDER && ( @@ -138,6 +179,14 @@ export function Header() { )}
+ { + getApp().getMap().setOption("showMissionDrawings", !mapOptions.showMissionDrawings); + }} + tooltip="Show/Hide mission drawings" + /> { diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index ef015a01..c3d68cef 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -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)