import { decimalToRGBA } from "../../other/utils"; import { getApp } from "../../olympusapp"; import { CommandModeOptionsChangedEvent, DrawingsInitEvent, DrawingsUpdatedEvent, MapOptionsChangedEvent, SessionDataLoadedEvent } from "../../events"; import { MapOptions } from "../../types/types"; import { Circle, DivIcon, Layer, LayerGroup, layerGroup, Marker, Polygon, Polyline } from "leaflet"; import { NavpointMarker } from "../markers/navpointmarker"; import { BLUE_COMMANDER, GAME_MASTER, NONE, RED_COMMANDER } from "../../constants/constants"; export abstract class DCSDrawing { #name: string; #parent: DCSDrawingsContainer; #weight: number; constructor(drawingData, parent: DCSDrawingsContainer) { this.#name = drawingData["name"]; this.#parent = parent; this.setWeight(drawingData); } getName() { return this.#name; } getParent() { return this.#parent; } toJSON() { return { name: this.#name, opacity: this.getOpacity(), visibility: this.getVisibility(), }; } setWeight(drawingData) { if (!drawingData.thickness) { return; } this.#weight = drawingData.thickness * 0.5; if (this.#weight === 0) { this.#weight = 0.1; } if (this.#weight > 1) { this.#weight = 1; } } getWeight() { return this.#weight; } 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: this.getWeight(), dashArray: dashArray, }); break; case "arrow": let weight = this.getWeight(); if (!weight || weight < 1) { weight = 1; } 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: weight, 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: this.getWeight(), 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: this.getWeight(), 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: this.getWeight(), 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: this.getWeight(), 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 DCSNavpoint extends DCSDrawing { #point: NavpointMarker; constructor(drawingData, parent) { super(drawingData, parent); this.#point = new NavpointMarker([drawingData.lat, drawingData.lng], drawingData.callsignStr, drawingData.comment); this.setVisibility(true); } getLayer() { return this.#point; } getLabelLayer() { return this.#point; } setOpacity(opacity: number): void { if (opacity === this.#point.options.opacity) return; this.#point.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.#point)) this.#point.addTo(this.getParent().getLayerGroup()); //@ts-ignore Leaflet typings are wrong if (!visibility && this.getParent().getLayerGroup().hasLayer(this.#point)) this.#point.removeFrom(this.getParent().getLayerGroup()); if (visibility && !this.getParent().getVisibility()) this.getParent().setVisibility(true); getApp().getDrawingsManager().requestUpdateEventDispatch(); } getOpacity(): number { return this.#point.options.opacity ?? 1; } getVisibility(): boolean { return this.getParent().getLayerGroup().hasLayer(this.#point); } } 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) => { const layerIsAContainer = drawingsData[layerName]["name"] === undefined && drawingsData[layerName]["callsignStr"] === undefined; const layerIsNotEmpty = Object.keys(drawingsData[layerName]).length > 0; if (layerIsAContainer && layerIsNotEmpty) { 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"]; const isANavpoint = !!drawingsData[layerName]['callsignStr']; // 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; if (isANavpoint) { newDrawing = new DCSNavpoint(drawingsData[layerName], othersContainer); if (hasContainers) othersContainer.addDrawing(newDrawing); else this.addDrawing(newDrawing); if (othersContainer.getDrawings().length === 0) this.removeSubContainer(othersContainer); // Remove empty container return; } 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 // FIXME: it's not working for main containers. } } 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; #navpointsContainer: DCSDrawingsContainer; #updateEventRequested: boolean = false; #sessionDataDrawings = {}; #sessionDataNavpoints = {}; #initialized: boolean = false; #hiddenContainers: Record = {}; constructor() { const drawingsLayerGroup = new LayerGroup(); drawingsLayerGroup.addTo(getApp().getMap()); this.#drawingsContainer = new DCSDrawingsContainer("Mission drawings", drawingsLayerGroup); this.#navpointsContainer = new DCSDrawingsContainer("Navpoints", drawingsLayerGroup); MapOptionsChangedEvent.on((mapOptions: MapOptions) => { this.#drawingsContainer.setVisibility(mapOptions.showMissionDrawings); this.#navpointsContainer.setVisibility(mapOptions.showMissionNavpoints); }); SessionDataLoadedEvent.on((sessionData) => { this.#sessionDataDrawings = sessionData.drawings ?? {}; this.#sessionDataNavpoints = sessionData.navpoints ?? {}; if (this.#initialized) { if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]); if (this.#sessionDataNavpoints["Navpoints"]) this.#navpointsContainer.fromJSON(this.#sessionDataNavpoints["Navpoints"]); } this.#drawingsContainer.setVisibility(getApp().getMap().getOptions().showMissionDrawings); this.#navpointsContainer.setVisibility(getApp().getMap().getOptions().showMissionNavpoints); }); CommandModeOptionsChangedEvent.on((commandOptions) => { if (commandOptions.commandMode !== GAME_MASTER) { this.restrictCoalitionLayers(commandOptions.commandMode) } this.restoreHiddenLayers(commandOptions.commandMode); }) } initDrawings(data: { drawings: Record> }): boolean { if (data && data.drawings) { if (data.drawings.navpoints) { this.#navpointsContainer.initFromData(data.drawings.navpoints); delete data.drawings.navpoints; } this.#drawingsContainer.initFromData(data.drawings); if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]); this.#drawingsContainer.setVisibility(getApp().getMap().getOptions().showMissionDrawings); DrawingsInitEvent.dispatch(this.#drawingsContainer, this.#navpointsContainer); this.#initialized = true; return true; } else { console.error("Error while initializing drawings, empty data"); return false; } } private restrictContainer(containerName: string, targetContainer: any, hiddenKey: string) { const container = targetContainer.getSubContainers().find(c => c.getName().toLowerCase() === containerName.toLowerCase()); if (container) { this.#hiddenContainers[hiddenKey] = { parent: container['#parent'], container: container }; container.setVisibility(false); targetContainer.removeSubContainer(container); } } restrictCoalitionLayers(playerRole: string) { if (playerRole === RED_COMMANDER) { this.restrictContainer('Blue', this.#drawingsContainer, 'blue_drawings'); this.restrictContainer('blue', this.#navpointsContainer, 'blue_navpoints'); } else { this.restrictContainer('Red', this.#drawingsContainer, 'red_drawings'); this.restrictContainer('red', this.#navpointsContainer, 'red_navpoints'); } } private restoreContainer(key: string, targetContainer: any) { if (this.#hiddenContainers[key]) { const container = this.#hiddenContainers[key].container; targetContainer.addSubContainer(container); container.setVisibility(true); } } restoreHiddenLayers(playerRole: string) { const roleContainers: Record = { [RED_COMMANDER]: ['red_drawings', 'red_navpoints'], [BLUE_COMMANDER]: ['blue_drawings', 'blue_navpoints'], [GAME_MASTER]: ['red_drawings', 'red_navpoints', 'blue_drawings', 'blue_navpoints'] }; roleContainers[playerRole]?.forEach((key) => { const targetContainer = key.includes('drawings') ? this.#drawingsContainer : this.#navpointsContainer; this.restoreContainer(key, targetContainer); }); } getDrawingsContainer() { return this.#drawingsContainer; } getNavpointsContainer() { return this.#navpointsContainer; } requestUpdateEventDispatch() { if (this.#updateEventRequested) return; this.#updateEventRequested = true; window.setTimeout(() => { this.#updateEventRequested = false; DrawingsUpdatedEvent.dispatch(); }, 100); } }