From 222a296b4f9dcb8acdb27e721b7d4e9cbe79a275 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 27 Jun 2024 14:46:28 +0200 Subject: [PATCH] Readded map options, simplified ShortcutManager --- frontend/react/src/constants/constants.ts | 42 +- frontend/react/src/dom.d.ts | 2 + frontend/react/src/eventscontext.tsx | 2 +- frontend/react/src/interfaces.ts | 4 +- frontend/react/src/map/map.ts | 114 +- frontend/react/src/map/map.ts.bak | 1044 +++++++++++++++++ frontend/react/src/olympusapp.ts | 295 +---- frontend/react/src/other/eventsmanager.ts | 7 - frontend/react/src/other/manager.ts | 37 - frontend/react/src/server/servermanager.ts | 7 - frontend/react/src/shortcut/shortcut.ts | 2 +- .../react/src/shortcut/shortcutmanager.ts | 139 ++- frontend/react/src/statecontext.tsx | 4 +- frontend/react/src/types/types.ts | 7 +- frontend/react/src/ui/panels/header.tsx | 7 +- frontend/react/src/ui/panels/options.tsx | 73 +- frontend/react/src/ui/ui.tsx | 21 +- frontend/react/src/unit/unit.ts | 26 +- frontend/react/src/unit/unitsmanager.ts | 6 - 19 files changed, 1363 insertions(+), 476 deletions(-) create mode 100644 frontend/react/src/map/map.ts.bak delete mode 100644 frontend/react/src/other/eventsmanager.ts delete mode 100644 frontend/react/src/other/manager.ts diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 851ca79a..16f265b1 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -1,5 +1,7 @@ import { LatLng, LatLngBounds } from "leaflet"; -//import { MapMarkerVisibilityControl } from "../map/map"; +import { Context } from "../types/types"; + +export const DEFAULT_CONTEXT: Context = "default context"; export const UNITS_URI = "units"; export const WEAPONS_URI = "weapons"; @@ -154,25 +156,8 @@ export const mapBounds = { "SinaiMap": { bounds: new LatLngBounds([34.312222, 28.523333], [25.946944, 36.897778]), zoom: 4 }, } -export const mapMirrors = { - "DCS Map mirror 1": "https://maps.dcsolympus.com/maps", - "DCS Map mirror 2": "https://refugees.dcsolympus.com/maps" -} - -export const defaultMapLayers = { - "ArcGIS Satellite": { - urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", - minZoom: 1, - maxZoom: 19, - attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Mapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" - }, - "OpenStreetMap Mapnik": { - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - minZoom: 1, - maxZoom: 20, - attribution: '© OpenStreetMap contributors' - } -} +export const defaultMapMirrors = {}; +export const defaultMapLayers = {}; /* Map constants */ export const IDLE = "Idle"; @@ -185,18 +170,6 @@ export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"]; export const IADSDensities: { [key: string]: number } = { "AAA": 0.8, "SAM Site": 0.1, "Radar (EWR)": 0.05 }; export const GROUND_UNIT_AIR_DEFENCE_REGEX: RegExp = /(\b(AAA|SAM|MANPADS?|[mM]anpads?)|[sS]tinger\b)/; -export const HIDE_GROUP_MEMBERS = "Hide group members when zoomed out"; -export const SHOW_UNIT_LABELS = "Show unit labels (L)"; -export const SHOW_UNITS_ENGAGEMENT_RINGS = "Show units threat range rings (Q)"; -export const HIDE_UNITS_SHORT_RANGE_RINGS = "Hide short range units threat range rings (R)"; -export const SHOW_UNITS_ACQUISITION_RINGS = "Show units detection range rings (E)"; -export const FILL_SELECTED_RING = "Fill the threat range rings of selected units (F)"; -export const SHOW_UNIT_CONTACTS = "Show selected units contact lines"; -export const SHOW_UNIT_PATHS = "Show selected unit paths"; -export const SHOW_UNIT_TARGETS = "Show selected unit targets"; -export const DCS_LINK_PORT = "DCS Camera link port"; -export const DCS_LINK_RATIO = "DCS Camera zoom"; - export const MAP_OPTIONS_TOOLTIPS = { hideGroupMembers: "Hide group members when zoomed out", hideUnitsShortRangeRings: "Hide short range units threat range rings (R)", @@ -216,7 +189,8 @@ export const MAP_OPTIONS_DEFAULTS = { showUnitTargets: false, showUnitLabels: true, showUnitsEngagementRings: true, - showUnitsAcquisitionRings: true + showUnitsAcquisitionRings: true, + fillSelectedRing: false } export const MAP_HIDDEN_TYPES_DEFAULTS = { @@ -298,4 +272,4 @@ export const GROUPING_ZOOM_TRANSITION = 13; export const MAX_SHOTS_SCATTER = 3; export const MAX_SHOTS_INTENSITY = 3; -export const SHOTS_SCATTER_DEGREES = 10; \ No newline at end of file +export const SHOTS_SCATTER_DEGREES = 10; diff --git a/frontend/react/src/dom.d.ts b/frontend/react/src/dom.d.ts index e3daa2a3..2f6e8ac8 100644 --- a/frontend/react/src/dom.d.ts +++ b/frontend/react/src/dom.d.ts @@ -12,7 +12,9 @@ interface CustomEventMap { "mapStateChanged": CustomEvent, "mapContextMenu": CustomEvent, "mapOptionChanged": CustomEvent, + "mapSourceChanged": CustomEvent, "mapOptionsChanged": CustomEvent, // TODO not very clear, why the two options? + "configLoaded": CustomEvent, "commandModeOptionsChanged": CustomEvent, "contactsUpdated": CustomEvent, "activeCoalitionChanged": CustomEvent, diff --git a/frontend/react/src/eventscontext.tsx b/frontend/react/src/eventscontext.tsx index dbba9005..3d1d30bb 100644 --- a/frontend/react/src/eventscontext.tsx +++ b/frontend/react/src/eventscontext.tsx @@ -12,7 +12,7 @@ export const EventsContext = createContext({ toggleUnitControlMenuVisible: () => {}, toggleMeasureMenuVisible: () => {}, toggleDrawingMenuVisible: () => {}, - toggleOptionsMenuVisible: () => {}, + toggleOptionsMenuVisible: () => {} }) export const EventsProvider = EventsContext.Provider; diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index f6f33118..00dec8cd 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -1,5 +1,5 @@ import { LatLng } from "leaflet"; -import { Coalition } from "./types/types"; +import { Coalition, Context } from "./types/types"; class Airbase { @@ -264,7 +264,7 @@ export interface Listener { export interface ShortcutOptions { altKey?: boolean; callback: CallableFunction; - context?: string; + context?: Context; ctrlKey?: boolean; name?: string; shiftKey?: boolean; diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 1fd8e940..4b2e9be0 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -1,26 +1,17 @@ import * as L from "leaflet" import { getApp } from "../olympusapp"; import { BoxSelect } from "./boxselect"; -//import { MapContextMenu } from "../contextmenus/mapcontextmenu"; -//import { UnitContextMenu } from "../contextmenus/unitcontextmenu"; -//import { AirbaseContextMenu } from "../contextmenus/airbasecontextmenu"; -//import { Dropdown } from "../controls/dropdown"; import { Airbase } from "../mission/airbase"; import { Unit } from "../unit/unit"; -import { bearing, /*createCheckboxOption, createSliderInputOption, createTextInputOption,*/ deg2rad, getGroundElevation, getUnitCategoryByBlueprint, polyContains } from "../other/utils"; +import { bearing, deg2rad, getGroundElevation, polyContains } from "../other/utils"; import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; -import { mapMirrors, defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, /*MAP_MARKER_CONTROLS,*/ DCS_LINK_PORT, DCS_LINK_RATIO, MAP_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, SPAWN_UNIT, CONTEXT_ACTION } from "../constants/constants"; +import { defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, defaultMapMirrors, SPAWN_UNIT, CONTEXT_ACTION, MAP_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS } from "../constants/constants"; import { CoalitionArea } from "./coalitionarea/coalitionarea"; -//import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu"; import { DrawingCursor } from "./coalitionarea/drawingcursor"; -//import { AirbaseSpawnContextMenu } from "../contextmenus/airbasespawnmenu"; -//import { GestureHandling } from "leaflet-gesture-handling"; import { TouchBoxSelect } from "./touchboxselect"; import { DestinationPreviewHandle } from "./markers/destinationpreviewHandle"; -import { ContextActionSet } from "../unit/contextactionset"; -import { DCSLayer } from "./dcslayer"; import './markers/stylesheets/airbase.css' import './markers/stylesheets/bullseye.css' @@ -28,10 +19,11 @@ import './markers/stylesheets/units.css' // Temporary import './theme.css' -import { Coalition, MapHiddenTypes, MapOptions } from "../types/types"; -import { SpawnRequestTable, UnitBlueprint, UnitSpawnTable } from "../interfaces"; +import { MapHiddenTypes, MapOptions } from "../types/types"; +import { SpawnRequestTable } from "../interfaces"; import { ContextAction } from "../unit/contextaction"; +// Touch screen support temporarily disabled var hasTouchScreen = false; //if ("maxTouchPoints" in navigator) // hasTouchScreen = navigator.maxTouchPoints > 0; @@ -43,10 +35,6 @@ else //L.Map.addInitHook("addHandler", "gestureHandling", GestureHandling); -// TODO would be nice to convert to ts - yes -//require("../../node_modules/leaflet.nauticscale/dist/leaflet.nauticscale.js") -//require("../../node_modules/leaflet-path-drag/dist/index.js") - export class Map extends L.Map { /* Options */ #options: MapOptions = MAP_OPTIONS_DEFAULTS; @@ -105,6 +93,7 @@ export class Map extends L.Map { #longPressTimer: number = 0; #mapLayers: any = defaultMapLayers; + #mapMirrors: any = defaultMapMirrors; #layerName: string = ""; #cameraOptionsXmlHttp: XMLHttpRequest | null = null; #bradcastPositionXmlHttp: XMLHttpRequest | null = null; @@ -136,10 +125,8 @@ export class Map extends L.Map { this.#ID = ID; - this.setLayer("DCS Map mirror 2"); - /* Minimap */ - var minimapLayer = new L.TileLayer(this.#mapLayers[Object.keys(this.#mapLayers)[0]].urlTemplate, { minZoom: 0, maxZoom: 13 }); + var minimapLayer = new L.TileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", { minZoom: 0, maxZoom: 13 }); this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]); this.#miniMapPolyline = new L.Polyline([], { color: '#202831' }); this.#miniMapPolyline.addTo(this.#miniMapLayerGroup); @@ -218,13 +205,36 @@ export class Map extends L.Map { document.addEventListener("configLoaded", () => { let config = getApp().getConfig(); - if (config.additionalMaps) { - let additionalMaps = config.additionalMaps; + let layerSet = false; + + /* First load the map mirrors */ + if (config.mapMirrors) { + let mapMirrors = config.mapMirrors; + this.#mapMirrors = { + ...this.#mapMirrors, + ...mapMirrors + } + this.setLayerName(Object.keys(mapMirrors)[0]); + } + + /* Set the options, and if at least one mirror is available, select the first */ + if (Object.keys(this.#mapMirrors).length > 0) { + this.setLayerName(Object.keys(this.#mapMirrors)[0]); + layerSet = true; // Needed because this is async + } + + /* Then load the map layers */ + if (config.mapLayers) { + let mapLayers = config.mapLayers; this.#mapLayers = { ...this.#mapLayers, - ...additionalMaps + ...mapLayers } - //this.#mapSourceDropdown.setOptions(this.getLayers(), null); + } + + /* Append this options, and if no mirror was selected, select the first on (if available). Mirrors have the precedence */ + if (!layerSet && Object.keys(this.#mapLayers).length > 0) { + this.setLayerName(Object.keys(this.#mapLayers)[0]); } }) @@ -253,11 +263,11 @@ export class Map extends L.Map { }, 1000) } - setLayer(layerName: string) { - if (this.#layer != null) - this.removeLayer(this.#layer) + setLayerName(layerName: string) { + if (this.#layer) + this.removeLayer(this.#layer); - let theatre = getApp().getMissionManager()?.getTheatre() ?? "Nevada"; + let theatre = getApp().getMissionManager()?.getTheatre(); /* Normal or custom layers are handled here */ if (layerName in this.#mapLayers) { @@ -267,16 +277,18 @@ export class Map extends L.Map { return new L.TileLayer(layer.urlTemplate.replace("{theatre}", theatre.toLowerCase()), layer); }) this.#layer = new L.LayerGroup(layers); + this.#layer?.addTo(this); } else { this.#layer = new L.TileLayer(layerData.urlTemplate, layerData); + this.#layer?.addTo(this); } - /* DCS core layers are handled here */ - } else if (["DCS Map mirror 1", "DCS Map mirror 2"].includes(layerName) ) { - let layerData = this.#mapLayers["ArcGIS Satellite"]; - let layers = [new L.TileLayer(layerData.urlTemplate, layerData)]; + + /* Mirrored layers are handled here */ + } else if (Object.keys(this.#mapMirrors).includes(layerName) ) { + let layers: L.TileLayer[] = []; /* Load the configuration file */ - const mirror = mapMirrors[layerName as keyof typeof mapMirrors]; + const mirror = this.#mapMirrors[layerName as any]; const request = new Request(mirror + "/config.json"); fetch(request).then((response) => { if (response.status === 200) { @@ -299,12 +311,16 @@ export class Map extends L.Map { }) } this.#layerName = layerName; + + document.dispatchEvent(new CustomEvent("mapSourceChanged", {detail: layerName})); + } + + getLayerName() { + return this.#layerName; } getLayers() { - let layers = ["DCS Map mirror 1", "DCS Map mirror 2"]; - layers.push(...Object.keys(this.#mapLayers)); - return layers; + return Object.keys(this.#mapLayers); } /* State machine */ @@ -486,7 +502,7 @@ export class Map extends L.Map { const boundaries = this.#getMinimapBoundaries(); this.#miniMapPolyline.setLatLngs(boundaries[theatre as keyof typeof boundaries]); - this.setLayer(this.#layerName); + this.setLayerName(this.#layerName); } getMiniMapLayerGroup() { @@ -615,21 +631,21 @@ export class Map extends L.Map { } increaseCameraZoom() { - const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`); - if (slider instanceof HTMLInputElement) { - slider.value = String(Math.min(Number(slider.max), Number(slider.value) + 10)); - slider.dispatchEvent(new Event('input')); - slider.dispatchEvent(new Event('mouseup')); - } + //const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`); + //if (slider instanceof HTMLInputElement) { + // slider.value = String(Math.min(Number(slider.max), Number(slider.value) + 10)); + // slider.dispatchEvent(new Event('input')); + // slider.dispatchEvent(new Event('mouseup')); + //} } decreaseCameraZoom() { - const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`); - if (slider instanceof HTMLInputElement) { - slider.value = String(Math.max(Number(slider.min), Number(slider.value) - 10)); - slider.dispatchEvent(new Event('input')); - slider.dispatchEvent(new Event('mouseup')); - } + //const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`); + //if (slider instanceof HTMLInputElement) { + // slider.value = String(Math.max(Number(slider.min), Number(slider.value) - 10)); + // slider.dispatchEvent(new Event('input')); + // slider.dispatchEvent(new Event('mouseup')); + //} } /* Event handlers */ diff --git a/frontend/react/src/map/map.ts.bak b/frontend/react/src/map/map.ts.bak new file mode 100644 index 00000000..1fd8e940 --- /dev/null +++ b/frontend/react/src/map/map.ts.bak @@ -0,0 +1,1044 @@ +import * as L from "leaflet" +import { getApp } from "../olympusapp"; +import { BoxSelect } from "./boxselect"; +//import { MapContextMenu } from "../contextmenus/mapcontextmenu"; +//import { UnitContextMenu } from "../contextmenus/unitcontextmenu"; +//import { AirbaseContextMenu } from "../contextmenus/airbasecontextmenu"; +//import { Dropdown } from "../controls/dropdown"; +import { Airbase } from "../mission/airbase"; +import { Unit } from "../unit/unit"; +import { bearing, /*createCheckboxOption, createSliderInputOption, createTextInputOption,*/ deg2rad, getGroundElevation, getUnitCategoryByBlueprint, polyContains } from "../other/utils"; +import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; +import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; +import { ClickableMiniMap } from "./clickableminimap"; +import { mapMirrors, defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, /*MAP_MARKER_CONTROLS,*/ DCS_LINK_PORT, DCS_LINK_RATIO, MAP_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, SPAWN_UNIT, CONTEXT_ACTION } from "../constants/constants"; +import { CoalitionArea } from "./coalitionarea/coalitionarea"; +//import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu"; +import { DrawingCursor } from "./coalitionarea/drawingcursor"; +//import { AirbaseSpawnContextMenu } from "../contextmenus/airbasespawnmenu"; +//import { GestureHandling } from "leaflet-gesture-handling"; +import { TouchBoxSelect } from "./touchboxselect"; +import { DestinationPreviewHandle } from "./markers/destinationpreviewHandle"; +import { ContextActionSet } from "../unit/contextactionset"; +import { DCSLayer } from "./dcslayer"; + +import './markers/stylesheets/airbase.css' +import './markers/stylesheets/bullseye.css' +import './markers/stylesheets/units.css' + +// Temporary +import './theme.css' +import { Coalition, MapHiddenTypes, MapOptions } from "../types/types"; +import { SpawnRequestTable, UnitBlueprint, UnitSpawnTable } from "../interfaces"; +import { ContextAction } from "../unit/contextaction"; + +var hasTouchScreen = false; +//if ("maxTouchPoints" in navigator) +// hasTouchScreen = navigator.maxTouchPoints > 0; + +if (hasTouchScreen) + L.Map.addInitHook('addHandler', 'boxSelect', TouchBoxSelect); +else + L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); + +//L.Map.addInitHook("addHandler", "gestureHandling", GestureHandling); + +// TODO would be nice to convert to ts - yes +//require("../../node_modules/leaflet.nauticscale/dist/leaflet.nauticscale.js") +//require("../../node_modules/leaflet-path-drag/dist/index.js") + +export class Map extends L.Map { + /* Options */ + #options: MapOptions = MAP_OPTIONS_DEFAULTS; + #hiddenTypes: MapHiddenTypes = MAP_HIDDEN_TYPES_DEFAULTS; + + #ID: string; + #state: string; + #layer: L.TileLayer | L.LayerGroup | null = null; + + #spawnRequestTable: SpawnRequestTable | null = null; + + #preventLeftClick: boolean = false; + #leftClickTimer: number = 0; + #deafultPanDelta: number = 100; + #panInterval: number | null = null; + #panLeft: boolean = false; + #panRight: boolean = false; + #panUp: boolean = false; + #panDown: boolean = false; + + #lastMousePosition: L.Point = new L.Point(0, 0); + #lastMouseCoordinates: L.LatLng = new L.LatLng(0, 0); + + #shiftKey: boolean = false; + #ctrlKey: boolean = false; + #centerUnit: Unit | null = null; + + #miniMap: ClickableMiniMap | null = null; + #miniMapLayerGroup: L.LayerGroup; + #miniMapPolyline: L.Polyline; + + #temporaryMarkers: TemporaryUnitMarker[] = []; + + #selecting: boolean = false; + #isZooming: boolean = false; + #previousZoom: number = 0; + + #slaveDCSCamera: boolean = false; + #slaveDCSCameraAvailable: boolean = false; + #cameraControlTimer: number = 0; + #cameraControlPort: number = 3003; + #cameraControlMode: string = 'map'; + + #destinationGroupRotation: number = 0; + #computeDestinationRotation: boolean = false; + #destinationRotationCenter: L.LatLng | null = null; + #coalitionAreas: CoalitionArea[] = []; + + #destinationPreviewCursors: DestinationPreviewMarker[] = []; + #drawingCursor: DrawingCursor = new DrawingCursor(); + #destinationPreviewHandle: DestinationPreviewHandle = new DestinationPreviewHandle(new L.LatLng(0, 0)); + #destinationPreviewHandleLine: L.Polyline = new L.Polyline([], { color: "#000000", weight: 3, opacity: 0.5, smoothFactor: 1, dashArray: "4, 8" }); + #spawnCursor: TemporaryUnitMarker | null = null; + + #longPressHandled: boolean = false; + #longPressTimer: number = 0; + + #mapLayers: any = defaultMapLayers; + #layerName: string = ""; + #cameraOptionsXmlHttp: XMLHttpRequest | null = null; + #bradcastPositionXmlHttp: XMLHttpRequest | null = null; + #cameraZoomRatio: number = 1.0; + + #contextAction: null | ContextAction = null; + + /** + * + * @param ID - the ID of the HTML element which will contain the context menu + */ + constructor(ID: string) { + /* Init the leaflet map */ + super(ID, { + preferCanvas: true, + doubleClickZoom: false, + zoomControl: false, + boxZoom: false, + //@ts-ignore Needed because the boxSelect option is non-standard + boxSelect: true, + zoomAnimation: true, + maxBoundsViscosity: 1.0, + minZoom: 7, + keyboard: true, + keyboardPanDelta: 0, + gestureHandling: hasTouchScreen + }); + this.setView([37.23, -115.8], 10); + + this.#ID = ID; + + this.setLayer("DCS Map mirror 2"); + + /* Minimap */ + var minimapLayer = new L.TileLayer(this.#mapLayers[Object.keys(this.#mapLayers)[0]].urlTemplate, { minZoom: 0, maxZoom: 13 }); + this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]); + this.#miniMapPolyline = new L.Polyline([], { color: '#202831' }); + this.#miniMapPolyline.addTo(this.#miniMapLayerGroup); + + + /* Scale */ + //@ts-ignore TODO more hacking because the module is provided as a pure javascript module only + //L.control.scalenautic({ position: "topright", maxWidth: 300, nautic: true, metric: true, imperial: false }).addTo(this); + + /* Map source dropdown */ + //this.#mapSourceDropdown = new Dropdown("map-type", (layerName: string) => this.setLayer(layerName)); + //this.#mapSourceDropdown.setOptions(this.getLayers(), null); + // + ///* Visibility options dropdown */ + //this.#mapVisibilityOptionsDropdown = new Dropdown("map-visibility-options", (value: string) => { }); + + /* Init the state machine */ + this.#state = IDLE; + + /* Register event handles */ + this.on("click", (e: any) => this.#onClick(e)); + this.on("dblclick", (e: any) => this.#onDoubleClick(e)); + this.on("zoomstart", (e: any) => this.#onZoomStart(e)); + this.on("zoom", (e: any) => this.#onZoom(e)); + this.on("zoomend", (e: any) => this.#onZoomEnd(e)); + this.on("drag", (e: any) => this.centerOnUnit(null)); + this.on("contextmenu", (e: any) => this.#onContextMenu(e)); + this.on('selectionstart', (e: any) => this.#onSelectionStart(e)); + this.on('selectionend', (e: any) => this.#onSelectionEnd(e)); + this.on('mousedown', (e: any) => this.#onMouseDown(e)); + this.on('mouseup', (e: any) => this.#onMouseUp(e)); + this.on('mousemove', (e: any) => this.#onMouseMove(e)); + this.on('keydown', (e: any) => this.#onKeyDown(e)); + this.on('keyup', (e: any) => this.#onKeyUp(e)); + this.on('move', (e: any) => { if (this.#slaveDCSCamera) this.#broadcastPosition() }); + + /* Event listeners */ + document.addEventListener("hiddenTypesChanged", (ev: CustomEventInit) => { + Object.values(getApp().getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); + Object.values(getApp().getMissionManager().getAirbases()).forEach((airbase: Airbase) => { + if (this.getHiddenTypes().airbase) + airbase.removeFrom(this); + else + airbase.addTo(this); + }) + }); + + document.addEventListener("toggleCoalitionAreaDraw", (ev: CustomEventInit) => { + //this.getMapContextMenu().hide(); + this.deselectAllCoalitionAreas(); + if (ev.detail?.type == "polygon") { + if (this.getState() !== COALITIONAREA_DRAW_POLYGON) + this.setState(COALITIONAREA_DRAW_POLYGON); + else + this.setState(IDLE); + } + }); + + //document.addEventListener("unitUpdated", (ev: CustomEvent) => { + // if (this.#centerUnit != null && ev.detail == this.#centerUnit) + // this.#panToUnit(this.#centerUnit); + //}); + + document.addEventListener("mapOptionsChanged", () => { + this.getContainer().toggleAttribute("data-hide-labels", !this.getOptions().showUnitLabels); + //this.#cameraControlPort = this.getOptions()[DCS_LINK_PORT] as number; + //this.#cameraZoomRatio = 50 / (20 + (this.getOptions()[DCS_LINK_RATIO] as number)); + + if (this.#slaveDCSCamera) { + this.#broadcastPosition(); + window.setTimeout(() => { + this.#broadcastPosition(); + }, 500); // DCS does not always apply the altitude correctly at the first set when changing map type + } + }); + + document.addEventListener("configLoaded", () => { + let config = getApp().getConfig(); + if (config.additionalMaps) { + let additionalMaps = config.additionalMaps; + this.#mapLayers = { + ...this.#mapLayers, + ...additionalMaps + } + //this.#mapSourceDropdown.setOptions(this.getLayers(), null); + } + }) + + document.addEventListener("toggleCameraLinkStatus", () => { + // if (this.#slaveDCSCameraAvailable) { // Commented to experiment with usability + this.setSlaveDCSCamera(!this.#slaveDCSCamera); + // } + }) + + document.addEventListener("slewCameraToPosition", () => { + // if (this.#slaveDCSCameraAvailable) { // Commented to experiment with usability + this.#broadcastPosition(); + // } + }) + + /* Pan interval */ + this.#panInterval = window.setInterval(() => { + if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft) + this.panBy(new L.Point(((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.#deafultPanDelta * (this.#shiftKey ? 3 : 1), + ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.#deafultPanDelta * (this.#shiftKey ? 3 : 1))); + }, 20); + + /* Periodically check if the camera control endpoint is available */ + this.#cameraControlTimer = window.setInterval(() => { + this.#checkCameraPort(); + }, 1000) + } + + setLayer(layerName: string) { + if (this.#layer != null) + this.removeLayer(this.#layer) + + let theatre = getApp().getMissionManager()?.getTheatre() ?? "Nevada"; + + /* Normal or custom layers are handled here */ + if (layerName in this.#mapLayers) { + const layerData = this.#mapLayers[layerName]; + if (layerData instanceof Array) { + let layers = layerData.map((layer: any) => { + return new L.TileLayer(layer.urlTemplate.replace("{theatre}", theatre.toLowerCase()), layer); + }) + this.#layer = new L.LayerGroup(layers); + } else { + this.#layer = new L.TileLayer(layerData.urlTemplate, layerData); + } + /* DCS core layers are handled here */ + } else if (["DCS Map mirror 1", "DCS Map mirror 2"].includes(layerName) ) { + let layerData = this.#mapLayers["ArcGIS Satellite"]; + let layers = [new L.TileLayer(layerData.urlTemplate, layerData)]; + + /* Load the configuration file */ + const mirror = mapMirrors[layerName as keyof typeof mapMirrors]; + const request = new Request(mirror + "/config.json"); + fetch(request).then((response) => { + if (response.status === 200) { + return response.json(); + } else { + return {}; + } + }).then((res: any) => { + if ("alt-" + theatre.toLowerCase() in res) { + let template = `${mirror}/alt-${theatre.toLowerCase()}/{z}/{x}/{y}.png`; + layers.push(...res["alt-" + theatre.toLowerCase()].map((layerConfig: any) => { + return new L.TileLayer(template, {...layerConfig, crossOrigin: ""}); + })); + } + this.#layer = new L.LayerGroup(layers); + this.#layer?.addTo(this); + }).catch(() => { + this.#layer = new L.LayerGroup(layers); + this.#layer?.addTo(this); + }) + } + this.#layerName = layerName; + } + + getLayers() { + let layers = ["DCS Map mirror 1", "DCS Map mirror 2"]; + layers.push(...Object.keys(this.#mapLayers)); + return layers; + } + + /* State machine */ + setState(state: string, options?: { spawnRequestTable?: SpawnRequestTable, contextAction?: ContextAction }) { + this.#state = state; + + /* Operations to perform if you are NOT in a state */ + if (this.#state !== COALITIONAREA_DRAW_POLYGON) { + this.#deselectSelectedCoalitionArea(); + } + + /* Operations to perform if you ARE in a state */ + if (this.#state === SPAWN_UNIT) { + this.#spawnRequestTable = options?.spawnRequestTable ?? null; + this.#spawnCursor?.removeFrom(this); + this.#spawnCursor = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? 'blue'); + } + else if (this.#state === CONTEXT_ACTION ) { + this.#contextAction = options?.contextAction ?? null; + } + else if (this.#state === COALITIONAREA_DRAW_POLYGON) { + this.#coalitionAreas.push(new CoalitionArea([])); + this.#coalitionAreas[this.#coalitionAreas.length - 1].addTo(this); + } + + this.#updateCursor(); + + document.dispatchEvent(new CustomEvent("mapStateChanged", { detail: this.#state })); + } + + getState() { + return this.#state; + } + + deselectAllCoalitionAreas() { + this.#coalitionAreas.forEach((coalitionArea: CoalitionArea) => coalitionArea.setSelected(false)); + } + + deleteCoalitionArea(coalitionArea: CoalitionArea) { + if (this.#coalitionAreas.includes(coalitionArea)) + this.#coalitionAreas.splice(this.#coalitionAreas.indexOf(coalitionArea), 1); + if (this.hasLayer(coalitionArea)) + this.removeLayer(coalitionArea); + } + + setHiddenType(key: string, value: boolean) { + this.#hiddenTypes[key] = value; + document.dispatchEvent(new CustomEvent("hiddenTypesChanged")); + } + + getHiddenTypes() { + return this.#hiddenTypes; + } + + /* Context Menus */ + hideAllContextMenus() { + this.hideMapContextMenu(); + this.hideUnitContextMenu(); + this.hideAirbaseContextMenu(); + this.hideAirbaseSpawnMenu(); + this.hideCoalitionAreaContextMenu(); + } + + showMapContextMenu(x: number, y: number, latlng: L.LatLng) { + //this.hideAllContextMenus(); + //this.#mapContextMenu.show(x, y, latlng); + //document.dispatchEvent(new CustomEvent("mapContextMenu")); + } + + hideMapContextMenu() { + //this.#mapContextMenu.hide(); + //document.dispatchEvent(new CustomEvent("mapContextMenu")); + } + + getMapContextMenu() { + return null //this.#mapContextMenu; + } + + showUnitContextMenu(x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) { + //this.hideAllContextMenus(); + //this.#unitContextMenu.show(x, y, latlng); + } + + getUnitContextMenu() { + return null //this.#unitContextMenu; + } + + hideUnitContextMenu() { + //this.#unitContextMenu.hide(); + } + + showAirbaseContextMenu(airbase: Airbase, x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) { + //this.hideAllContextMenus(); + //this.#airbaseContextMenu.show(x, y, latlng); + //this.#airbaseContextMenu.setAirbase(airbase); + } + + getAirbaseContextMenu() { + return null //this.#airbaseContextMenu; + } + + hideAirbaseContextMenu() { + //this.#airbaseContextMenu.hide(); + } + + showAirbaseSpawnMenu(airbase: Airbase, x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) { + //this.hideAllContextMenus(); + //this.#airbaseSpawnMenu.show(x, y); + //this.#airbaseSpawnMenu.setAirbase(airbase); + } + + getAirbaseSpawnMenu() { + return null //this.#airbaseSpawnMenu; + } + + hideAirbaseSpawnMenu() { + //this.#airbaseSpawnMenu.hide(); + } + + showCoalitionAreaContextMenu(x: number, y: number, latlng: L.LatLng, coalitionArea: CoalitionArea) { + //this.hideAllContextMenus(); + //this.#coalitionAreaContextMenu.show(x, y, latlng); + //this.#coalitionAreaContextMenu.setCoalitionArea(coalitionArea); + } + + getCoalitionAreaContextMenu() { + return null //this.#coalitionAreaContextMenu; + } + + hideCoalitionAreaContextMenu() { + //this.#coalitionAreaContextMenu.hide(); + } + + getMousePosition() { + return this.#lastMousePosition; + } + + getMouseCoordinates() { + return this.#lastMouseCoordinates; + } + + centerOnUnit(unit: Unit | null) { + if (unit !== null) { + this.options.scrollWheelZoom = 'center'; + this.#centerUnit = unit; + } + else { + this.options.scrollWheelZoom = undefined; + this.#centerUnit = null; + } + this.#updateCursor(); + } + + getCenteredOnUnit() { + return this.#centerUnit; + } + + setTheatre(theatre: string) { + var bounds = new L.LatLngBounds([-90, -180], [90, 180]); + var miniMapZoom = 5; + if (theatre in mapBounds) { + bounds = mapBounds[theatre as keyof typeof mapBounds].bounds; + miniMapZoom = mapBounds[theatre as keyof typeof mapBounds].zoom; + } + + this.setView(bounds.getCenter(), 8); + + if (this.#miniMap) + this.#miniMap.remove(); + + //@ts-ignore // Needed because some of the inputs are wrong in the original module interface + this.#miniMap = new ClickableMiniMap(this.#miniMapLayerGroup, { position: "topright", width: 192 * 1.5, height: 108 * 1.5, zoomLevelFixed: miniMapZoom, centerFixed: bounds.getCenter() }).addTo(this); + this.#miniMap.disableInteractivity(); + this.#miniMap.getMap().on("click", (e: any) => { + if (this.#miniMap) + this.setView(e.latlng); + }) + + const boundaries = this.#getMinimapBoundaries(); + this.#miniMapPolyline.setLatLngs(boundaries[theatre as keyof typeof boundaries]); + + this.setLayer(this.#layerName); + } + + getMiniMapLayerGroup() { + return this.#miniMapLayerGroup; + } + + handleMapPanning(e: any) { + if (e.type === "keyup") { + switch (e.code) { + case "KeyA": + case "ArrowLeft": + this.#panLeft = false; + break; + case "KeyD": + case "ArrowRight": + this.#panRight = false; + break; + case "KeyW": + case "ArrowUp": + this.#panUp = false; + break; + case "KeyS": + case "ArrowDown": + this.#panDown = false; + break; + } + } + else { + switch (e.code) { + case 'KeyA': + case 'ArrowLeft': + this.#panLeft = true; + break; + case 'KeyD': + case 'ArrowRight': + this.#panRight = true; + break; + case 'KeyW': + case 'ArrowUp': + this.#panUp = true; + break; + case 'KeyS': + case 'ArrowDown': + this.#panDown = true; + break; + } + } + } + + addTemporaryMarker(latlng: L.LatLng, name: string, coalition: string, commandHash?: string) { + var marker = new TemporaryUnitMarker(latlng, name, coalition, commandHash); + marker.addTo(this); + this.#temporaryMarkers.push(marker); + return marker; + } + + getSelectedCoalitionArea() { + return this.#coalitionAreas.find((area: CoalitionArea) => { return area.getSelected() }); + } + + bringCoalitionAreaToBack(coalitionArea: CoalitionArea) { + coalitionArea.bringToBack(); + this.#coalitionAreas.splice(this.#coalitionAreas.indexOf(coalitionArea), 1); + this.#coalitionAreas.unshift(coalitionArea); + } + + setOption(key, value) { + this.#options[key] = value; + document.dispatchEvent(new CustomEvent("mapOptionsChanged")); + } + + getOptions() { + return this.#options; + } + + isZooming() { + return this.#isZooming; + } + + getPreviousZoom() { + return this.#previousZoom; + } + + getIsUnitProtected(unit: Unit) { + //const toggles = this.#mapMarkerVisibilityControls.reduce((list, control: MapMarkerVisibilityControl) => { + // if (control.isProtected) { + // list = list.concat(control.toggles); + // } + // return list; + //}, [] as string[]); + // + //if (toggles.length === 0) + // return false; + // + //return toggles.some((toggle: string) => { + // // Specific coding for robots - extend later if needed + // return (toggle === "dcs" && !unit.getControlled() && !unit.getHuman()); + //}); + return false; + } + + getMapMarkerVisibilityControls() { + return null //this.#mapMarkerVisibilityControls; + } + + setSlaveDCSCamera(newSlaveDCSCamera: boolean) { + this.#slaveDCSCamera = newSlaveDCSCamera; + let button = document.getElementById("camera-link-control"); + button?.classList.toggle("off", !newSlaveDCSCamera); + if (this.#slaveDCSCamera) { + this.#broadcastPosition(); + window.setTimeout(() => { + this.#broadcastPosition(); + }, 500); // DCS does not always apply the altitude correctly at the first set when changing map type + } + } + + setCameraControlMode(newCameraControlMode: string) { + this.#cameraControlMode = newCameraControlMode; + if (this.#slaveDCSCamera) { + this.#broadcastPosition(); + window.setTimeout(() => { + this.#broadcastPosition(); + }, 500); // DCS does not always apply the altitude correctly at the first set when changing map type + } + } + + increaseCameraZoom() { + const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`); + if (slider instanceof HTMLInputElement) { + slider.value = String(Math.min(Number(slider.max), Number(slider.value) + 10)); + slider.dispatchEvent(new Event('input')); + slider.dispatchEvent(new Event('mouseup')); + } + } + + decreaseCameraZoom() { + const slider = document.querySelector(`label[title="${DCS_LINK_RATIO}"] input`); + if (slider instanceof HTMLInputElement) { + slider.value = String(Math.max(Number(slider.min), Number(slider.value) - 10)); + slider.dispatchEvent(new Event('input')); + slider.dispatchEvent(new Event('mouseup')); + } + } + + /* Event handlers */ + #onClick(e: any) { + if (!this.#preventLeftClick) { + this.hideAllContextMenus(); + if (this.#state === IDLE) { + this.deselectAllCoalitionAreas(); + } + else if (this.#state === SPAWN_UNIT) { + if (this.#spawnRequestTable !== null) { + const location = this.getMouseCoordinates(); + getApp().getUnitsManager().spawnUnits( + this.#spawnRequestTable.category, + [ this.#spawnRequestTable.unit ], + this.#spawnRequestTable.coalition, + false, + undefined, + undefined, + (hash) => { + this.addTemporaryMarker(location, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", hash) + } + ) + } + } + else if (this.#state === COALITIONAREA_DRAW_POLYGON) { + if (this.getSelectedCoalitionArea()?.getEditing()) { + this.getSelectedCoalitionArea()?.addTemporaryLatLng(e.latlng); + } + else { + this.deselectAllCoalitionAreas(); + } + } + else { + this.setState(IDLE); + getApp().getUnitsManager().deselectAllUnits(); + } + } + } + + #onDoubleClick(e: any) { + + } + + #onContextMenu(e: any) { + /* A long press will show the point action context menu */ + window.clearInterval(this.#longPressTimer); + if (this.#longPressHandled) { + this.#longPressHandled = false; + return; + } + + this.hideMapContextMenu(); + if (this.#state === IDLE) { + if (this.#state == IDLE) { + this.showMapContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); + var clickedCoalitionArea: CoalitionArea | null = null; + + /* Coalition areas are ordered in the #coalitionAreas array according to their zindex. Select the upper one */ + for (let coalitionArea of this.#coalitionAreas) { + if (polyContains(e.latlng, coalitionArea)) { + if (coalitionArea.getSelected()) + clickedCoalitionArea = coalitionArea; + //else + // this.getMapContextMenu()?.setCoalitionArea(coalitionArea); + } + } + if (clickedCoalitionArea) + this.showCoalitionAreaContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng, clickedCoalitionArea); + } + } + else if (this.#state === MOVE_UNIT) { + if (!e.originalEvent.shiftKey) { + if (!e.originalEvent.ctrlKey) { + getApp().getUnitsManager().clearDestinations(); + } + getApp().getUnitsManager().addDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation) + + this.#destinationGroupRotation = 0; + this.#destinationRotationCenter = null; + this.#computeDestinationRotation = false; + } + } + else if (this.#state === CONTEXT_ACTION) { + if (this.#contextAction) + this.#contextAction.executeCallback(null, e.latlng); + } + else { + this.setState(IDLE); + } + } + + #onSelectionStart(e: any) { + this.#selecting = true; + this.#updateCursor(); + } + + #onSelectionEnd(e: any) { + this.#selecting = false; + clearTimeout(this.#leftClickTimer); + this.#preventLeftClick = true; + this.#leftClickTimer = window.setTimeout(() => { + this.#preventLeftClick = false; + }, 200); + getApp().getUnitsManager().selectFromBounds(e.selectionBounds); + this.#updateCursor(); + } + + #onMouseDown(e: any) { + this.hideAllContextMenus(); + + if (this.#state == MOVE_UNIT) { + this.#destinationGroupRotation = 0; + this.#destinationRotationCenter = null; + this.#computeDestinationRotation = false; + if (e.originalEvent.button == 2) { + this.#computeDestinationRotation = true; + this.#destinationRotationCenter = this.getMouseCoordinates(); + } + } + + //this.#longPressTimer = window.setTimeout(() => { + // this.hideMapContextMenu(); + // this.#longPressHandled = true; + // + // if (e.originalEvent.button != 2 || e.originalEvent.ctrlKey || e.originalEvent.shiftKey) + // return; + // + // var contextActionSet = new ContextActionSet(); + // var units = getApp().getUnitsManager().getSelectedUnits(); + // units.forEach((unit: Unit) => { + // unit.appendContextActions(contextActionSet, null, e.latlng); + // }) + // + // if (Object.keys(contextActionSet.getContextActions()).length > 0) { + // getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); + // //getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet); + // } + //}, 150); + //this.#longPressHandled = false; + } + + #onMouseUp(e: any) { + if (this.#state === MOVE_UNIT && e.originalEvent.button == 2 && e.originalEvent.shiftKey) { + if (!e.originalEvent.ctrlKey) { + getApp().getUnitsManager().clearDestinations(); + } + getApp().getUnitsManager().addDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation) + + this.#destinationGroupRotation = 0; + this.#destinationRotationCenter = null; + this.#computeDestinationRotation = false; + } + } + + #onMouseMove(e: any) { + this.#lastMousePosition.x = e.originalEvent.x; + this.#lastMousePosition.y = e.originalEvent.y; + this.#lastMouseCoordinates = this.mouseEventToLatLng(e.originalEvent); + + this.#updateCursor(); + + if (this.#state === MOVE_UNIT) { + /* Update the position of the destination cursors depeding on mouse rotation */ + if (this.#computeDestinationRotation && this.#destinationRotationCenter != null) + this.#destinationGroupRotation = -bearing(this.#destinationRotationCenter.lat, this.#destinationRotationCenter.lng, this.getMouseCoordinates().lat, this.getMouseCoordinates().lng); + this.#updateDestinationCursors(); + } + else if (this.#state === SPAWN_UNIT) { + this.#updateSpawnCursor(); + } + else if (this.#state === COALITIONAREA_DRAW_POLYGON && e.latlng !== undefined) { + this.#drawingCursor.setLatLng(e.latlng); + /* Update the polygon being drawn with the current position of the mouse cursor */ + this.getSelectedCoalitionArea()?.moveActiveVertex(e.latlng); + } + } + + #onKeyDown(e: any) { + this.#shiftKey = e.originalEvent.shiftKey; + this.#ctrlKey = e.originalEvent.ctrlKey; + this.#updateCursor(); + this.#updateDestinationCursors(); + } + + #onKeyUp(e: any) { + this.#shiftKey = e.originalEvent.shiftKey; + this.#ctrlKey = e.originalEvent.ctrlKey; + this.#updateCursor(); + this.#updateDestinationCursors(); + } + + #onZoomStart(e: any) { + this.#previousZoom = this.getZoom(); + if (this.#centerUnit != null) + this.#panToUnit(this.#centerUnit); + this.#isZooming = true; + } + + #onZoom(e: any) { + if (this.#centerUnit != null) + this.#panToUnit(this.#centerUnit); + } + + #onZoomEnd(e: any) { + this.#isZooming = false; + } + + #broadcastPosition() { + if (this.#bradcastPositionXmlHttp?.readyState !== 4 && this.#bradcastPositionXmlHttp !== null) + return + + getGroundElevation(this.getCenter(), (response: string) => { + var groundElevation: number | null = null; + try { + groundElevation = parseFloat(response); + this.#bradcastPositionXmlHttp = new XMLHttpRequest(); + /* Using 127.0.0.1 instead of localhost because the LuaSocket version used in DCS only listens to IPv4. This avoids the lag caused by the + browser if it first tries to send the request on the IPv6 address for localhost */ + this.#bradcastPositionXmlHttp.open("POST", `http://127.0.0.1:${this.#cameraControlPort}`); + + const C = 40075016.686; + let mpp = C * Math.cos(deg2rad(this.getCenter().lat)) / Math.pow(2, this.getZoom() + 8); + let d = mpp * 1920; + let alt = d / 2 * 1 / Math.tan(deg2rad(40)) * this.#cameraZoomRatio; + alt = Math.min(alt, 50000); + this.#bradcastPositionXmlHttp.send(JSON.stringify({ lat: this.getCenter().lat, lng: this.getCenter().lng, alt: alt + groundElevation, mode: this.#cameraControlMode })); + } catch { + console.warn("broadcastPosition: could not retrieve ground elevation") + } + }); + } + + /* */ + #panToUnit(unit: Unit) { + var unitPosition = new L.LatLng(unit.getPosition().lat, unit.getPosition().lng); + this.setView(unitPosition, this.getZoom(), { animate: false }); + this.#updateCursor(); + this.#updateDestinationCursors(); + } + + #getMinimapBoundaries() { + /* Draw the limits of the maps in the minimap*/ + return minimapBoundaries; + } + + #deselectSelectedCoalitionArea() { + this.getSelectedCoalitionArea()?.setSelected(false); + } + + /* Cursors */ + #updateCursor() { + /* If the ctrl key is being pressed or we are performing an area selection, show the default cursor */ + if (this.#ctrlKey || this.#selecting) { + /* Hide all non default cursors */ + this.#hideDestinationCursors(); + this.#hideDrawingCursor(); + this.#hideSpawnCursor(); + + this.#showDefaultCursor(); + } else { + /* Hide all the unnecessary cursors depending on the active state */ + if (this.#state !== IDLE) this.#hideDefaultCursor(); + if (this.#state !== MOVE_UNIT) this.#hideDestinationCursors(); + if (this.#state !== SPAWN_UNIT) this.#hideSpawnCursor(); + if (this.#state !== COALITIONAREA_DRAW_POLYGON) this.#hideDrawingCursor(); + + /* Show the active cursor depending on the active state */ + if (this.#state === IDLE) this.#showDefaultCursor(); + else if (this.#state === MOVE_UNIT) this.#showDestinationCursors(); + else if (this.#state === SPAWN_UNIT) this.#showSpawnCursor(); + else if (this.#state === COALITIONAREA_DRAW_POLYGON) this.#showDrawingCursor(); + } + } + + #showDefaultCursor() { + document.getElementById(this.#ID)?.classList.remove("hidden-cursor"); + } + + #hideDefaultCursor() { + document.getElementById(this.#ID)?.classList.add("hidden-cursor"); + } + + #showDestinationCursors() { + const singleCursor = !this.#shiftKey; + const selectedUnitsCount = getApp().getUnitsManager().getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true }).length; + if (singleCursor) { + this.#hideDestinationCursors(); + } + else if (!singleCursor) { + if (selectedUnitsCount > 1) { + while (this.#destinationPreviewCursors.length > selectedUnitsCount) { + this.removeLayer(this.#destinationPreviewCursors[0]); + this.#destinationPreviewCursors.splice(0, 1); + } + + this.#destinationPreviewHandleLine.addTo(this); + this.#destinationPreviewHandle.addTo(this); + + while (this.#destinationPreviewCursors.length < selectedUnitsCount) { + var cursor = new DestinationPreviewMarker(this.getMouseCoordinates(), { interactive: false }); + cursor.addTo(this); + this.#destinationPreviewCursors.push(cursor); + } + + this.#updateDestinationCursors(); + } + } + } + + #updateDestinationCursors() { + const selectedUnitsCount = getApp().getUnitsManager().getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true }).length; + if (selectedUnitsCount > 1) { + const groupLatLng = this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : this.getMouseCoordinates(); + if (this.#destinationPreviewCursors.length == 1) + this.#destinationPreviewCursors[0].setLatLng(this.getMouseCoordinates()); + else { + Object.values(getApp().getUnitsManager().computeGroupDestination(groupLatLng, this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => { + if (idx < this.#destinationPreviewCursors.length) + this.#destinationPreviewCursors[idx].setLatLng(this.#shiftKey ? latlng : this.getMouseCoordinates()); + }) + }; + + this.#destinationPreviewHandleLine.setLatLngs([groupLatLng, this.getMouseCoordinates()]); + this.#destinationPreviewHandle.setLatLng(this.getMouseCoordinates()); + } else { + this.#hideDestinationCursors(); + } + } + + #hideDestinationCursors() { + /* Remove all the destination cursors */ + this.#destinationPreviewCursors.forEach((marker: L.Marker) => { + this.removeLayer(marker); + }) + this.#destinationPreviewCursors = []; + + this.#destinationPreviewHandleLine.removeFrom(this); + this.#destinationPreviewHandle.removeFrom(this); + + /* Reset the variables used to compute the rotation of the group cursors */ + this.#destinationGroupRotation = 0; + this.#computeDestinationRotation = false; + this.#destinationRotationCenter = null; + } + + #showDrawingCursor() { + this.#hideDefaultCursor(); + if (!this.hasLayer(this.#drawingCursor)) + this.#drawingCursor.addTo(this); + } + + #hideDrawingCursor() { + this.#drawingCursor.setLatLng(new L.LatLng(0, 0)); + if (this.hasLayer(this.#drawingCursor)) + this.#drawingCursor.removeFrom(this); + } + + #showSpawnCursor() { + this.#spawnCursor?.addTo(this); + } + + #updateSpawnCursor() { + this.#spawnCursor?.setLatLng(this.getMouseCoordinates()); + } + + #hideSpawnCursor() { + this.#spawnCursor?.removeFrom(this); + } + + #setSlaveDCSCameraAvailable(newSlaveDCSCameraAvailable: boolean) { + this.#slaveDCSCameraAvailable = newSlaveDCSCameraAvailable; + let linkButton = document.getElementById("camera-link-control"); + if (linkButton) { + if (!newSlaveDCSCameraAvailable) { + //this.setSlaveDCSCamera(false); // Commented to experiment with usability + linkButton.classList.add("red"); + linkButton.title = "Camera link to DCS is not available"; + } else { + linkButton.classList.remove("red"); + linkButton.title = "Link/Unlink DCS camera with Olympus position"; + } + } + } + + /* Check if the camera control plugin is available. Right now this will only change the color of the button, no changes in functionality */ + #checkCameraPort() { + if (this.#cameraOptionsXmlHttp?.readyState !== 4) + this.#cameraOptionsXmlHttp?.abort() + + this.#cameraOptionsXmlHttp = new XMLHttpRequest(); + + /* Using 127.0.0.1 instead of localhost because the LuaSocket version used in DCS only listens to IPv4. This avoids the lag caused by the + browser if it first tries to send the request on the IPv6 address for localhost */ + this.#cameraOptionsXmlHttp.open("OPTIONS", `http://127.0.0.1:${this.#cameraControlPort}`); + this.#cameraOptionsXmlHttp.onload = (res: any) => { + if (this.#cameraOptionsXmlHttp !== null && this.#cameraOptionsXmlHttp.status == 204) + this.#setSlaveDCSCameraAvailable(true); + else + this.#setSlaveDCSCameraAvailable(false); + }; + this.#cameraOptionsXmlHttp.onerror = (res: any) => { + this.#setSlaveDCSCameraAvailable(false); + } + this.#cameraOptionsXmlHttp.ontimeout = (res: any) => { + this.#setSlaveDCSCameraAvailable(false); + } + this.#cameraOptionsXmlHttp.timeout = 500; + this.#cameraOptionsXmlHttp.send(""); + } +} + diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index 972d58d4..a893928d 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -14,39 +14,25 @@ export function getApp() { import { Map } from "./map/map"; import { MissionManager } from "./mission/missionmanager"; -//import { ConnectionStatusPanel } from "./panels/connectionstatuspanel"; -//import { HotgroupPanel } from "./panels/hotgrouppanel"; -//import { LogPanel } from "./panels/logpanel"; -//import { MouseInfoPanel } from "./panels/mouseinfopanel"; -//import { ServerStatusPanel } from "./panels/serverstatuspanel"; -//import { UnitControlPanel } from "./panels/unitcontrolpanel"; -//import { UnitInfoPanel } from "./panels/unitinfopanel"; -//import { PluginsManager } from "./plugin/pluginmanager"; -//import { Popup } from "./popups/popup"; import { ShortcutManager } from "./shortcut/shortcutmanager"; -//import { CommandModeToolbar } from "./toolbars/commandmodetoolbar"; -//import { PrimaryToolbar } from "./toolbars/primarytoolbar"; import { UnitsManager } from "./unit/unitsmanager"; import { WeaponsManager } from "./weapon/weaponsmanager"; -//import { Manager } from "./other/manager"; import { ServerManager } from "./server/servermanager"; -import { sha256 } from 'js-sha256'; -import { BLUE_COMMANDER, FILL_SELECTED_RING, GAME_MASTER, HIDE_UNITS_SHORT_RANGE_RINGS, RED_COMMANDER, SHOW_UNITS_ACQUISITION_RINGS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNIT_LABELS } from "./constants/constants"; +import { BLUE_COMMANDER, DEFAULT_CONTEXT, GAME_MASTER, RED_COMMANDER } from "./constants/constants"; import { aircraftDatabase } from "./unit/databases/aircraftdatabase"; import { helicopterDatabase } from "./unit/databases/helicopterdatabase"; import { groundUnitDatabase } from "./unit/databases/groundunitdatabase"; import { navyUnitDatabase } from "./unit/databases/navyunitdatabase"; -//import { UnitListPanel } from "./panels/unitlistpanel"; -//import { ContextManager } from "./context/contextmanager"; -//import { Context } from "./context/context"; +import { Coalition, Context } from "./types/types"; + export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; export var IP = window.location.toString(); export var connectedToServer = true; // Temporary export class OlympusApp { /* Global data */ - #activeCoalition: string = "blue"; + #activeCoalition: Coalition = "blue"; #latestVersion: string|undefined = undefined; #config: any = {}; @@ -54,54 +40,31 @@ export class OlympusApp { #map: Map | null = null; /* Managers */ - //#contextManager!: ContextManager; - //#dialogManager!: Manager; #missionManager: MissionManager | null = null; - //#panelsManager: Manager | null = null; - //#pluginsManager: PluginsManager | null = null; - //#popupsManager: Manager | null = null; #serverManager: ServerManager | null = null; - #shortcutManager!: ShortcutManager; - //#toolbarsManager: Manager | null = null; + #shortcutManager: ShortcutManager | null = null; #unitsManager: UnitsManager | null = null; #weaponsManager: WeaponsManager | null = null; + //#pluginsManager: // TODO + + /* Current context */ + #context: Context = DEFAULT_CONTEXT; constructor() { } - // TODO add checks on null - getDialogManager() { - return null //this.#dialogManager as Manager; + getCurrentContext() { + return this.#context; } getMap() { return this.#map as Map; } - getCurrentContext() { - return null //this.getContextManager().getCurrentContext() as Context; - } - - getContextManager() { - return null // this.#contextManager as ContextManager; - } - getServerManager() { return this.#serverManager as ServerManager; } - getPanelsManager() { - return null // this.#panelsManager as Manager; - } - - getPopupsManager() { - return null // this.#popupsManager as Manager; - } - - getToolbarsManager() { - return null // this.#toolbarsManager as Manager; - } - getShortcutManager() { return this.#shortcutManager as ShortcutManager; } @@ -118,15 +81,17 @@ export class OlympusApp { return this.#missionManager as MissionManager; } + /* TODO getPluginsManager() { return null // this.#pluginsManager as PluginsManager; } + */ /** Set the active coalition, i.e. the currently controlled coalition. A game master can change the active coalition, while a commander is bound to his/her coalition * * @param newActiveCoalition */ - setActiveCoalition(newActiveCoalition: string) { + setActiveCoalition(newActiveCoalition: Coalition) { if (this.getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER) { this.#activeCoalition = newActiveCoalition; document.dispatchEvent(new CustomEvent("activeCoalitionChanged")); @@ -137,7 +102,7 @@ export class OlympusApp { * * @returns The active coalition */ - getActiveCoalition() { + getActiveCoalition(): Coalition { if (this.getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER) return this.#activeCoalition; else { @@ -182,77 +147,22 @@ export class OlympusApp { return navyUnitDatabase; } - /** Set a message in the login splash screen - * - * @param status The message to show in the login splash screen - */ - setLoginStatus(status: string) { - const el = document.querySelector("#login-status") as HTMLElement; - if (el) - el.dataset["status"] = status; - } - start() { /* Initialize base functionalitites */ - //this.#contextManager = new ContextManager(); - //this.#contextManager.add( "olympus", {} ); - this.#map = new Map('map-container'); this.#missionManager = new MissionManager(); - //this.#panelsManager = new Manager(); - //this.#popupsManager = new Manager(); this.#serverManager = new ServerManager(); this.#shortcutManager = new ShortcutManager(); - //this.#toolbarsManager = new Manager(); this.#unitsManager = new UnitsManager(); this.#weaponsManager = new WeaponsManager(); - // Toolbars - //this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar")) - // .add("commandModeToolbar", new CommandModeToolbar("command-mode-toolbar")); -// - //// Panels - //this.getPanelsManager() - // .add("connectionStatus", new ConnectionStatusPanel("connection-status-panel")) - // .add("hotgroup", new HotgroupPanel("hotgroup-panel")) - // .add("mouseInfo", new MouseInfoPanel("mouse-info-panel")) - // .add("log", new LogPanel("log-panel")) - // .add("serverStatus", new ServerStatusPanel("server-status-panel")) - // .add("unitControl", new UnitControlPanel("unit-control-panel")) - // .add("unitInfo", new UnitInfoPanel("unit-info-panel")) - // .add("unitList", new UnitListPanel("unit-list-panel", "unit-list-panel-content")) -// - //// Popups - //this.getPopupsManager() - // .add("infoPopup", new Popup("info-popup")); - // - //this.#pluginsManager = new PluginsManager(); - /* Set the address of the server */ this.getServerManager().setAddress(window.location.href.split('?')[0]); /* Setup all global events */ this.#setupEvents(); - /* Set the splash background image to a random image */ - let splashScreen = document.getElementById("splash-screen") as HTMLElement; - let i = Math.round(Math.random() * 7 + 1); - - if (splashScreen) { - new Promise((resolve, reject) => { - const image = new Image(); - image.addEventListener('load', resolve); - image.addEventListener('error', resolve); - image.src = `/resources/theme/images/splash/${i}.jpg`; - }).then(() => { - splashScreen.style.backgroundImage = `url('/resources/theme/images/splash/${i}.jpg')`; - let loadingScreen = document.getElementById("loading-screen") as HTMLElement; - loadingScreen.classList.add("fade-out"); - window.setInterval(() => { loadingScreen.classList.add("hide"); }, 1000); - }) - } - /* Check if we are running the latest version */ const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json"); fetch(request).then((response) => { @@ -285,181 +195,14 @@ export class OlympusApp { }) } + getConfig() { + return this.#config; + } + #setupEvents() { - /* Generic clicks */ - document.addEventListener("click", (ev) => { - if (ev instanceof MouseEvent && ev.target instanceof HTMLElement) { - const target = ev.target; - - if (target.classList.contains("olympus-dialog-close")) { - target.closest("div.olympus-dialog")?.classList.add("hide"); - } - - const triggerElement = target.closest("[data-on-click]"); - - if (triggerElement instanceof HTMLElement) { - const eventName: string = triggerElement.dataset.onClick || ""; - let params = JSON.parse(triggerElement.dataset.onClickParams || "{}"); - params._element = triggerElement; - - if (eventName) { - document.dispatchEvent(new CustomEvent(eventName, { - detail: params - })); - } - } - } - }); - - const shortcutManager = this.getShortcutManager(); - shortcutManager.addKeyboardShortcut("togglePause", { - "altKey": false, - "callback": () => { - this.getServerManager().setPaused(!this.getServerManager().getPaused()); - }, - "code": "Space", - "context": "olympus", - "ctrlKey": false - }).addKeyboardShortcut("deselectAll", { - "callback": (ev: KeyboardEvent) => { - this.getUnitsManager().deselectAllUnits(); - }, - "code": "Escape", - "context": "olympus" - }).addKeyboardShortcut("toggleUnitLabels", { - "altKey": false, - "callback": () => { - const chk = document.querySelector(`label[title="${SHOW_UNIT_LABELS}"] input[type="checkbox"]`); - if (chk instanceof HTMLElement) { - chk.click(); - } - }, - "code": "KeyL", - "context": "olympus", - "ctrlKey": false, - "shiftKey": false - }).addKeyboardShortcut("toggleAcquisitionRings", { - "altKey": false, - "callback": () => { - const chk = document.querySelector(`label[title="${SHOW_UNITS_ACQUISITION_RINGS}"] input[type="checkbox"]`); - if (chk instanceof HTMLElement) { - chk.click(); - } - }, - "code": "KeyE", - "context": "olympus", - "ctrlKey": false, - "shiftKey": false - }).addKeyboardShortcut("toggleEngagementRings", { - "altKey": false, - "callback": () => { - const chk = document.querySelector(`label[title="${SHOW_UNITS_ENGAGEMENT_RINGS}"] input[type="checkbox"]`); - if (chk instanceof HTMLElement) { - chk.click(); - } - }, - "code": "KeyQ", - "context": "olympus", - "ctrlKey": false, - "shiftKey": false - }).addKeyboardShortcut("toggleHideShortEngagementRings", { - "altKey": false, - "callback": () => { - const chk = document.querySelector(`label[title="${HIDE_UNITS_SHORT_RANGE_RINGS}"] input[type="checkbox"]`); - if (chk instanceof HTMLElement) { - chk.click(); - } - }, - "code": "KeyR", - "context": "olympus", - "ctrlKey": false, - "shiftKey": false - }).addKeyboardShortcut("toggleFillEngagementRings", { - "altKey": false, - "callback": () => { - const chk = document.querySelector(`label[title="${FILL_SELECTED_RING}"] input[type="checkbox"]`); - if (chk instanceof HTMLElement) { - chk.click(); - } - }, - "code": "KeyF", - "context": "olympus", - "ctrlKey": false, - "shiftKey": false - }).addKeyboardShortcut("increaseCameraZoom", { - "altKey": true, - "callback": () => { - //this.getMap().increaseCameraZoom(); - }, - "code": "Equal", - "context": "olympus", - "ctrlKey": false, - "shiftKey": false - }).addKeyboardShortcut("decreaseCameraZoom", { - "altKey": true, - "callback": () => { - //this.getMap().decreaseCameraZoom(); - }, - "code": "Minus", - "context": "olympus", - "ctrlKey": false, - "shiftKey": false - }); - - ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].forEach(code => { - shortcutManager.addKeyboardShortcut(`pan${code}keydown`, { - "altKey": false, - "callback": (ev: KeyboardEvent) => { - //this.getMap().handleMapPanning(ev); - }, - "code": code, - "context": "olympus", - "ctrlKey": false, - "event": "keydown" - }); - - shortcutManager.addKeyboardShortcut(`pan${code}keyup`, { - "callback": (ev: KeyboardEvent) => { - //this.getMap().handleMapPanning(ev); - }, - "code": code, - "context": "olympus" - }); - }); - - const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"]; - - digits.forEach(code => { - shortcutManager.addKeyboardShortcut(`hotgroup${code}`, { - "altKey": false, - "callback": (ev: KeyboardEvent) => { - if (ev.ctrlKey && ev.shiftKey) - this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); // "Select hotgroup X in addition to any units already selected" - else if (ev.ctrlKey && !ev.shiftKey) - this.getUnitsManager().setHotgroup(parseInt(ev.code.substring(5))); // "These selected units are hotgroup X (forget any previous membership)" - else if (!ev.ctrlKey && ev.shiftKey) - this.getUnitsManager().addToHotgroup(parseInt(ev.code.substring(5))); // "Add (append) these units to hotgroup X (in addition to any existing members)" - else - this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it." - }, - "code": code - }); - - // Stop hotgroup controls sending the browser to another tab - document.addEventListener("keydown", (ev: KeyboardEvent) => { - if (ev.code === code && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) { - ev.preventDefault(); - } - }); - }); - /* Reload the page, used to mimic a restart of the app */ document.addEventListener("reloadPage", () => { location.reload(); }) } - - getConfig() { - return this.#config; - } } \ No newline at end of file diff --git a/frontend/react/src/other/eventsmanager.ts b/frontend/react/src/other/eventsmanager.ts deleted file mode 100644 index 9173b2b1..00000000 --- a/frontend/react/src/other/eventsmanager.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Manager } from "./manager"; - -export abstract class EventsManager extends Manager { - constructor() { - super(); - } -} \ No newline at end of file diff --git a/frontend/react/src/other/manager.ts b/frontend/react/src/other/manager.ts deleted file mode 100644 index c889e713..00000000 --- a/frontend/react/src/other/manager.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Context } from "../context/context"; - -export class Manager { - - #items: { [key: string]: any } = {}; - - constructor() { - - } - - add(name: string, item: any) { - const regex = new RegExp("^[a-z][a-z0-9]{2,}$", "i"); - if (regex.test(name) === false) { - throw new Error(`Item name "${name}" does not match regex: ${regex.toString()}.`); - } - - if (this.#items.hasOwnProperty(name)) { - throw new Error(`Item with name "${name}" already exists.`); - } - - this.#items[name] = item; - return this; - } - - get(name: string) { - if (this.#items.hasOwnProperty(name)) { - return this.#items[name]; - } else { - return false; - } - } - - getAll() { - return this.#items; - } - -} \ No newline at end of file diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index e0a853e5..3a65bcce 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -1,13 +1,7 @@ import { LatLng } from 'leaflet'; import { getApp } from '../olympusapp'; import { AIRBASES_URI, BULLSEYE_URI, COMMANDS_URI, LOGS_URI, MISSION_URI, NONE, ROEs, UNITS_URI, WEAPONS_URI, emissionsCountermeasures, reactionsToThreat } from '../constants/constants'; -//import { ServerStatusPanel } from '../panels/serverstatuspanel'; -//import { LogPanel } from '../panels/logpanel'; -//import { Popup } from '../popups/popup'; -//import { ConnectionStatusPanel } from '../panels/connectionstatuspanel'; import { AirbasesData, BullseyesData, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from '../interfaces'; -import { zeroAppend } from '../other/utils'; -import { SiTheregister } from 'react-icons/si'; export class ServerManager { #connected: boolean = false; @@ -89,7 +83,6 @@ export class ServerManager { } else if (xmlHttp.status == 401) { /* Bad credentials */ console.error("Incorrect username/password"); - getApp().setLoginStatus("failed"); errorCallback && errorCallback(xmlHttp.status); } else { /* Failure, probably disconnected */ diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts index 77228d79..c022b948 100644 --- a/frontend/react/src/shortcut/shortcut.ts +++ b/frontend/react/src/shortcut/shortcut.ts @@ -20,7 +20,7 @@ export class ShortcutKeyboard extends Shortcut { super(config); document.addEventListener(config.event, (ev: any) => { - if ( typeof config.context === "string" && !getApp().getContextManager().currentContextIs( config.context ) ) { + if ( typeof config.context === "string" && getApp().getCurrentContext() !== config.context ) { return; } diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts index 8d86f741..28e34ed8 100644 --- a/frontend/react/src/shortcut/shortcutmanager.ts +++ b/frontend/react/src/shortcut/shortcutmanager.ts @@ -1,18 +1,16 @@ +import { DEFAULT_CONTEXT } from "../constants/constants"; import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces"; -import { Manager } from "../other/manager"; - +import { getApp } from "../olympusapp"; import { ShortcutKeyboard, ShortcutMouse } from "./shortcut"; -export class ShortcutManager extends Manager { +export class ShortcutManager { + #items: { [key: string]: any } = {}; #keysBeingHeld: string[] = []; #keyDownCallbacks: CallableFunction[] = []; #keyUpCallbacks: CallableFunction[] = []; constructor() { - - super(); - document.addEventListener("keydown", (ev: KeyboardEvent) => { if (this.#keysBeingHeld.indexOf(ev.code) < 0) { this.#keysBeingHeld.push(ev.code) @@ -25,20 +23,137 @@ export class ShortcutManager extends Manager { this.#keyUpCallbacks.forEach(callback => callback(ev)); }); - } + this.addKeyboardShortcut("togglePause", { + "altKey": false, + "callback": () => { + getApp().getServerManager().setPaused(!getApp().getServerManager().getPaused()); + }, + "code": "Space", + "context": DEFAULT_CONTEXT, + "ctrlKey": false + }).addKeyboardShortcut("deselectAll", { + "callback": (ev: KeyboardEvent) => { + getApp().getUnitsManager().deselectAllUnits(); + }, + "code": "Escape", + "context": DEFAULT_CONTEXT + }).addKeyboardShortcut("toggleUnitLabels", { + "altKey": false, + "callback": () => { getApp().getMap().setOption("showUnitLabels", !getApp().getMap().getOptions().showUnitLabels) }, + "code": "KeyL", + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "shiftKey": false + }).addKeyboardShortcut("toggleAcquisitionRings", { + "altKey": false, + "callback": () => { getApp().getMap().setOption("showUnitsAcquisitionRings", !getApp().getMap().getOptions().showUnitsAcquisitionRings) }, + "code": "KeyE", + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "shiftKey": false + }).addKeyboardShortcut("toggleEngagementRings", { + "altKey": false, + "callback": () => { getApp().getMap().setOption("showUnitsEngagementRings", !getApp().getMap().getOptions().showUnitsEngagementRings) }, + "code": "KeyQ", + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "shiftKey": false + }).addKeyboardShortcut("toggleHideShortEngagementRings", { + "altKey": false, + "callback": () => { getApp().getMap().setOption("hideUnitsShortRangeRings", !getApp().getMap().getOptions().hideUnitsShortRangeRings) }, + "code": "KeyR", + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "shiftKey": false + }).addKeyboardShortcut("toggleDetectionLines", { + "altKey": false, + "callback": () => { getApp().getMap().setOption("showUnitTargets", !getApp().getMap().getOptions().showUnitTargets) }, + "code": "KeyF", + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "shiftKey": false + }).addKeyboardShortcut("toggleGroupMembers", { + "altKey": false, + "callback": () => { getApp().getMap().setOption("hideGroupMembers", !getApp().getMap().getOptions().hideGroupMembers) }, + "code": "KeyG", + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "shiftKey": false + }).addKeyboardShortcut("increaseCameraZoom", { + "altKey": true, + "callback": () => { + //getApp().getMap().increaseCameraZoom(); + }, + "code": "Equal", + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "shiftKey": false + }).addKeyboardShortcut("decreaseCameraZoom", { + "altKey": true, + "callback": () => { + //getApp().getMap().decreaseCameraZoom(); + }, + "code": "Minus", + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "shiftKey": false + }); - add(name: string, shortcut: any) { - console.error("ShortcutManager:add() cannot be used. Use addKeyboardShortcut or addMouseShortcut."); - return this; + ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].forEach(code => { + this.addKeyboardShortcut(`pan${code}keydown`, { + "altKey": false, + "callback": (ev: KeyboardEvent) => { + //getApp().getMap().handleMapPanning(ev); + }, + "code": code, + "context": DEFAULT_CONTEXT, + "ctrlKey": false, + "event": "keydown" + }); + + this.addKeyboardShortcut(`pan${code}keyup`, { + "callback": (ev: KeyboardEvent) => { + //getApp().getMap().handleMapPanning(ev); + }, + "code": code, + "context": DEFAULT_CONTEXT + }); + }); + + const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"]; + + digits.forEach(code => { + this.addKeyboardShortcut(`hotgroup${code}`, { + "altKey": false, + "callback": (ev: KeyboardEvent) => { + if (ev.ctrlKey && ev.shiftKey) + getApp().getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); // "Select hotgroup X in addition to any units already selected" + else if (ev.ctrlKey && !ev.shiftKey) + getApp().getUnitsManager().setHotgroup(parseInt(ev.code.substring(5))); // "These selected units are hotgroup X (forget any previous membership)" + else if (!ev.ctrlKey && ev.shiftKey) + getApp().getUnitsManager().addToHotgroup(parseInt(ev.code.substring(5))); // "Add (append) these units to hotgroup X (in addition to any existing members)" + else + getApp().getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it." + }, + "code": code + }); + + // Stop hotgroup controls sending the browser to another tab + document.addEventListener("keydown", (ev: KeyboardEvent) => { + if (ev.code === code && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) { + ev.preventDefault(); + } + }); + }); } addKeyboardShortcut(name: string, shortcutKeyboardOptions: ShortcutKeyboardOptions) { - super.add(name, new ShortcutKeyboard(shortcutKeyboardOptions)); + this.#items[name] = new ShortcutKeyboard(shortcutKeyboardOptions); return this; } addMouseShortcut(name: string, shortcutMouseOptions: ShortcutMouseOptions) { - super.add(name, new ShortcutMouse(shortcutMouseOptions)); + this.#items[name] = new ShortcutMouse(shortcutMouseOptions); return this; } diff --git a/frontend/react/src/statecontext.tsx b/frontend/react/src/statecontext.tsx index ba263e33..6b81c578 100644 --- a/frontend/react/src/statecontext.tsx +++ b/frontend/react/src/statecontext.tsx @@ -9,7 +9,9 @@ export const StateContext = createContext({ drawingMenuVisible: false, optionsMenuVisible: false, mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS, - mapOptions: MAP_OPTIONS_DEFAULTS + mapOptions: MAP_OPTIONS_DEFAULTS, + mapSources: [] as string[], + activeMapSource: "" }) export const StateProvider = StateContext.Provider; diff --git a/frontend/react/src/types/types.ts b/frontend/react/src/types/types.ts index 638683a7..b14c1317 100644 --- a/frontend/react/src/types/types.ts +++ b/frontend/react/src/types/types.ts @@ -17,7 +17,8 @@ export type MapOptions = { showUnitTargets: boolean, showUnitLabels: boolean, showUnitsEngagementRings: boolean, - showUnitsAcquisitionRings: boolean + showUnitsAcquisitionRings: boolean, + fillSelectedRing: boolean } export type MapHiddenTypes = { @@ -48,4 +49,6 @@ export type MGRS = { zoneNumber: string } -export type Coalition = "blue" | "neutral" | "red"; \ No newline at end of file +export type Coalition = "blue" | "neutral" | "red"; + +export type Context = string; diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index b9bbae73..b36a7f60 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -78,9 +78,10 @@ export function Header() { {}}> {}} tooltip="Activate/deactivate camera plugin" /> - - DCS Sat - DCS Alt + + {appState.mapSources.map((source) => { + return getApp().getMap().setLayerName(source)}>{ source } + })} diff --git a/frontend/react/src/ui/panels/options.tsx b/frontend/react/src/ui/panels/options.tsx index 751c13e5..4846d14c 100644 --- a/frontend/react/src/ui/panels/options.tsx +++ b/frontend/react/src/ui/panels/options.tsx @@ -1,16 +1,15 @@ import React from "react"; import { Menu } from "./components/menu"; -import { faArrowRightLong, faCheckCircle, faDatabase, faExternalLink, faExternalLinkAlt, faFile, faFileAlt, faFileExport, faFileImport, faTimesCircle } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faGithub } from "@fortawesome/free-brands-svg-icons"; import { OlCheckbox } from "../components/olcheckbox"; -import { OlLabelToggle } from "../components/ollabeltoggle"; import { OlRangeSlider } from "../components/olrangeslider"; import { OlNumberInput } from "../components/olnumberinput"; +import { MapOptions } from "../../types/types"; +import { getApp } from "../../olympusapp"; export function Options(props: { open: boolean, onClose: () => void, + options: MapOptions, children?: JSX.Element | JSX.Element[], }) { return -
-
- { }}> - Toggle Unit Labels +
+
{ getApp().getMap().setOption("showUnitLabels", !props.options.showUnitLabels) }} + > + { }}> + Show Unit Labels + L
-
- { }}> - Toggle Threat Rings +
{ getApp().getMap().setOption("showUnitsEngagementRings", !props.options.showUnitsEngagementRings) }} + > + { }}> + Show Threat Rings + Q
-
- { }}> - Toggle Detection rings +
{ getApp().getMap().setOption("showUnitsAcquisitionRings", !props.options.showUnitsAcquisitionRings) }} + > + { }}> + Show Detection rings + E
-
- { }}> - Toggle Detection lines +
{ getApp().getMap().setOption("showUnitTargets", !props.options.showUnitTargets) }} + > + { }}> + Show Detection lines + F
-
- { }}> - Toggle Radar lines +
{ getApp().getMap().setOption("hideUnitsShortRangeRings", !props.options.hideUnitsShortRangeRings) }} + > + { }}> + Hide Short range Rings + R
-
- { }}> - Toggle Something Else +
{ getApp().getMap().setOption("hideGroupMembers", !props.options.hideGroupMembers) }} + > + { }}> + Hide Group members + G
+ + {/*
@@ -71,7 +96,7 @@ export function Options(props: { onChange={(ev) => { }} />
-
+
*/}
} \ No newline at end of file diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index c6ef52e2..8ae0dbe5 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -42,6 +42,8 @@ export function UI() { var [checkingPassword, setCheckingPassword] = useState(false); var [loginError, setLoginError] = useState(false); var [commandMode, setCommandMode] = useState(null as null | string); + var [mapSources, setMapSources] = useState([] as string[]); + var [activeMapSource, setActiveMapSource] = useState(""); document.addEventListener("hiddenTypesChanged", (ev) => { setMapHiddenTypes({ ...getApp().getMap().getHiddenTypes() }); @@ -57,6 +59,20 @@ export function UI() { } }) + document.addEventListener("mapSourceChanged", (ev) => { + var source = (ev as CustomEvent).detail; + if (source !== activeMapSource) + setActiveMapSource(source); + }) + + + document.addEventListener("configLoaded", (ev) => { + let config = getApp().getConfig(); + var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers)); + setMapSources(sources); + setActiveMapSource(sources[0]); + }) + function hideAllMenus() { setMainMenuVisible(false); setSpawnMenuVisible(false); @@ -111,7 +127,9 @@ export function UI() { drawingMenuVisible: drawingMenuVisible, optionsMenuVisible: optionsMenuVisible, mapOptions: mapOptions, - mapHiddenTypes: mapHiddenTypes + mapHiddenTypes: mapHiddenTypes, + mapSources: mapSources, + activeMapSource: activeMapSource }}> setOptionsMenuVisible(false)} + options={mapOptions} /> diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 7baa955b..27e13d50 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -5,7 +5,7 @@ import { CustomMarker } from '../map/markers/custommarker'; import { SVGInjector } from '@tanem/svg-injector'; import { UnitDatabase } from './databases/unitdatabase'; import { TargetMarker } from '../map/markers/targetmarker'; -import { DLINK, DataIndexes, GAME_MASTER, HIDE_GROUP_MEMBERS, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, ROEs, RWR, SHOW_UNIT_CONTACTS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, VISUAL, emissionsCountermeasures, reactionsToThreat, states, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, GROUPING_ZOOM_TRANSITION, MAX_SHOTS_SCATTER, SHOTS_SCATTER_DEGREES, GROUND_UNIT_AIR_DEFENCE_REGEX } from '../constants/constants'; +import { DLINK, DataIndexes, GAME_MASTER, IDLE, IRST, MOVE_UNIT, OPTIC, RADAR, ROEs, RWR, VISUAL, emissionsCountermeasures, reactionsToThreat, states, GROUPING_ZOOM_TRANSITION, MAX_SHOTS_SCATTER, SHOTS_SCATTER_DEGREES, GROUND_UNIT_AIR_DEFENCE_REGEX } from '../constants/constants'; import { DataExtractor } from '../server/dataextractor'; import { groundUnitDatabase } from './databases/groundunitdatabase'; import { navyUnitDatabase } from './databases/navyunitdatabase'; @@ -708,7 +708,7 @@ export abstract class Unit extends CustomMarker { /* Hide the unit if it does not belong to the commanded coalition and it is not detected by a method that can pinpoint its location (RWR does not count) */ (!this.belongsToCommandedCoalition() && (this.#detectionMethods.length == 0 || (this.#detectionMethods.length == 1 && this.#detectionMethods[0] === RWR))) || /* Hide the unit if grouping is activated, the unit is not the group leader, it is not selected, and the zoom is higher than the grouping threshold */ - (getApp().getMap().getOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && !this.getSelected() && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && + (getApp().getMap().getOptions().hideGroupMembers && !this.#isLeader && !this.getSelected() && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && (this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0)))); /* Force dead units to be hidden */ @@ -1245,7 +1245,7 @@ export abstract class Unit extends CustomMarker { } #drawPath() { - if (this.#activePath != undefined && getApp().getMap().getOptions()[SHOW_UNIT_PATHS]) { + if (this.#activePath != undefined && getApp().getMap().getOptions().showUnitPaths) { var points: LatLng[] = []; points.push(new LatLng(this.#position.lat, this.#position.lng)); @@ -1289,7 +1289,7 @@ export abstract class Unit extends CustomMarker { #drawContacts() { this.#clearContacts(); - if (getApp().getMap().getOptions()[SHOW_UNIT_CONTACTS]) { + if (getApp().getMap().getOptions().showUnitContacts) { for (let index in this.#contacts) { var contactData = this.#contacts[index]; var contact: Unit | Weapon | null; @@ -1364,12 +1364,12 @@ export abstract class Unit extends CustomMarker { if (engagementRange !== this.#engagementCircle.getRadius()) this.#engagementCircle.setRadius(engagementRange); - this.#engagementCircle.options.fillOpacity = this.getSelected() && getApp().getMap().getOptions()[FILL_SELECTED_RING] ? 0.3 : 0; + this.#engagementCircle.options.fillOpacity = this.getSelected() && getApp().getMap().getOptions().fillSelectedRing ? 0.3 : 0; /* Acquisition circles */ - var shortAcquisitionRangeCheck = (acquisitionRange > nmToM(3) || !getApp().getMap().getOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]); + var shortAcquisitionRangeCheck = (acquisitionRange > nmToM(3) || !getApp().getMap().getOptions().hideUnitsShortRangeRings); - if (getApp().getMap().getOptions()[SHOW_UNITS_ACQUISITION_RINGS] && shortAcquisitionRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) { + if (getApp().getMap().getOptions().showUnitsAcquisitionRings && shortAcquisitionRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) { if (!getApp().getMap().hasLayer(this.#acquisitionCircle)) { this.#acquisitionCircle.addTo(getApp().getMap()); switch (this.getCoalition()) { @@ -1393,8 +1393,8 @@ export abstract class Unit extends CustomMarker { } /* Engagement circles */ - var shortEngagementRangeCheck = (engagementRange > nmToM(3) || !getApp().getMap().getOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]); - if (getApp().getMap().getOptions()[SHOW_UNITS_ENGAGEMENT_RINGS] && shortEngagementRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) { + var shortEngagementRangeCheck = (engagementRange > nmToM(3) || !getApp().getMap().getOptions().hideUnitsShortRangeRings); + if (getApp().getMap().getOptions().showUnitsEngagementRings && shortEngagementRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) { if (!getApp().getMap().hasLayer(this.#engagementCircle)) { this.#engagementCircle.addTo(getApp().getMap()); switch (this.getCoalition()) { @@ -1428,10 +1428,10 @@ export abstract class Unit extends CustomMarker { } #drawTarget() { - if (this.#targetPosition.lat != 0 && this.#targetPosition.lng != 0 && getApp().getMap().getOptions()[SHOW_UNIT_PATHS]) { + if (this.#targetPosition.lat != 0 && this.#targetPosition.lng != 0 && getApp().getMap().getOptions().showUnitPaths) { this.#drawTargetPosition(this.#targetPosition); } - else if (this.#targetID != 0 && getApp().getMap().getOptions()[SHOW_UNIT_TARGETS]) { + else if (this.#targetID != 0 && getApp().getMap().getOptions().showUnitTargets) { const target = getApp().getUnitsManager().getUnitByID(this.#targetID); if (target && (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || (this.belongsToCommandedCoalition() && getApp().getUnitsManager().getUnitDetectedMethods(target).some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))))) { this.#drawTargetPosition(target.getPosition()); @@ -1648,7 +1648,7 @@ export class GroundUnit extends Unit { /* When a unit is a leader of a group, the map is zoomed out and grouping when zoomed out is enabled, check if the unit should be shown as a specific group. This is used to show a SAM battery instead of the group leader */ getDatabaseEntry() { let unitWhenGrouped: string | undefined | null = null; - if (!this.getSelected() && this.getIsLeader() && getApp().getMap().getOptions()[HIDE_GROUP_MEMBERS] && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION) { + if (!this.getSelected() && this.getIsLeader() && getApp().getMap().getOptions().hideGroupMembers && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION) { unitWhenGrouped = this.getDatabase()?.getByName(this.getName())?.unitWhenGrouped ?? null; let member = this.getGroupMembers().reduce((prev: Unit | null, unit: Unit, index: number) => { if (unit.getDatabaseEntry()?.unitWhenGrouped != undefined) @@ -1665,7 +1665,7 @@ export class GroundUnit extends Unit { /* When we zoom past the grouping limit, grouping is enabled and the unit is a leader, we redraw the unit to apply any possible grouped marker */ checkZoomRedraw(): boolean { - return (this.getIsLeader() && getApp().getMap().getOptions()[HIDE_GROUP_MEMBERS] as boolean && + return (this.getIsLeader() && getApp().getMap().getOptions().hideGroupMembers as boolean && (getApp().getMap().getZoom() >= GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() < GROUPING_ZOOM_TRANSITION || getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() >= GROUPING_ZOOM_TRANSITION)) } diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 4fcba3b9..136d5c94 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -1192,9 +1192,6 @@ export class UnitsManager { * */ copy(units: Unit[] | null = null) { - //if (!getApp().getContextManager().getCurrentContext().getAllowUnitCopying()) - // return; - if (units === null) units = this.getSelectedUnits(); @@ -1212,9 +1209,6 @@ export class UnitsManager { * @returns True if units were pasted successfully */ paste() { - //if (!getApp().getContextManager().getCurrentContext().getAllowUnitPasting()) - // return; - let spawnPoints = 0; /* If spawns are restricted, check that the user has the necessary spawn points */