import * as L from "leaflet" import { getApp } from ".."; 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, createTextInputOption, deg2rad, getGroundElevation, polyContains } from "../other/utils"; import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { SVGInjector } from '@tanem/svg-injector' import { 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 } 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"; 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 type MapMarkerVisibilityControl = { "category"?: string; "image": string; "isProtected"?: boolean, "name": string, "protectable"?: boolean, "toggles": string[], "tooltip": string } export class Map extends L.Map { #ID: string; #state: string; #layer: L.TileLayer | L.LayerGroup | 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); #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" }); #longPressHandled: boolean = false; #longPressTimer: number = 0; #mapContextMenu: MapContextMenu = new MapContextMenu("map-contextmenu"); #unitContextMenu: UnitContextMenu = new UnitContextMenu("unit-contextmenu"); #airbaseContextMenu: AirbaseContextMenu = new AirbaseContextMenu("airbase-contextmenu"); #airbaseSpawnMenu: AirbaseSpawnContextMenu = new AirbaseSpawnContextMenu("airbase-spawn-contextmenu"); #coalitionAreaContextMenu: CoalitionAreaContextMenu = new CoalitionAreaContextMenu("coalition-area-contextmenu"); #mapSourceDropdown: Dropdown; #mapLayers: any = defaultMapLayers; #mapMarkerVisibilityControls: MapMarkerVisibilityControl[] = MAP_MARKER_CONTROLS; #mapVisibilityOptionsDropdown: Dropdown; #optionButtons: { [key: string]: HTMLButtonElement[] } = {} #visibilityOptions: { [key: string]: boolean | string | number } = {} #hiddenTypes: string[] = []; /** * * @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(Object.keys(this.#mapLayers)[0]); /* 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.getLayers()); /* 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('drag', (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("toggleCoalitionVisibility", (ev: CustomEventInit) => { const el = ev.detail._element; el?.classList.toggle("off"); this.setHiddenType(ev.detail.coalition, !el?.classList.contains("off")); Object.values(getApp().getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); }); document.addEventListener("toggleMarkerVisibility", (ev: CustomEventInit) => { const el = ev.detail._element; el?.classList.toggle("off"); ev.detail.types.forEach((type: string) => this.setHiddenType(type, !el?.classList.contains("off"))); Object.values(getApp().getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); if (ev.detail.types.includes("airbase")) { Object.values(getApp().getMissionManager().getAirbases()).forEach((airbase: Airbase) => { if (el?.classList.contains("off")) 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.getVisibilityOptions()[SHOW_UNIT_LABELS]); this.#cameraControlPort = this.getVisibilityOptions()[DCS_LINK_PORT] as number; }); document.addEventListener("configLoaded", () => { let config = getApp().getConfig(); if (config.additionalMaps) { let additionalMaps = config.additionalMaps; this.#mapLayers = { ...this.#mapLayers, ...additionalMaps } this.#mapSourceDropdown.setOptions(this.getLayers()); } }) 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) /* Option buttons */ this.#createUnitMarkerControlButtons(); /* Create the checkboxes to select the advanced visibility options */ this.addVisibilityOption(DCS_LINK_PORT, 3003, { min: 1024, max: 65535 }); this.#mapVisibilityOptionsDropdown.addHorizontalDivider(); this.addVisibilityOption(SHOW_UNIT_CONTACTS, false); this.addVisibilityOption(HIDE_GROUP_MEMBERS, true); this.addVisibilityOption(SHOW_UNIT_PATHS, true); this.addVisibilityOption(SHOW_UNIT_TARGETS, true); this.addVisibilityOption(SHOW_UNIT_LABELS, true); this.addVisibilityOption(SHOW_UNITS_ENGAGEMENT_RINGS, true); this.addVisibilityOption(SHOW_UNITS_ACQUISITION_RINGS, true); this.addVisibilityOption(HIDE_UNITS_SHORT_RANGE_RINGS, true); /* this.addVisibilityOption(FILL_SELECTED_RING, false); Removed since currently broken: TODO fix!*/ } addVisibilityOption(option: string, defaultValue: boolean | number | string, options?: { [key: string]: any }) { this.#visibilityOptions[option] = defaultValue; if (typeof defaultValue === 'boolean') this.#mapVisibilityOptionsDropdown.addOptionElement(createCheckboxOption(option, option, defaultValue as boolean, (ev: any) => { this.#setVisibilityOption(option, ev); }, options)); else if (typeof defaultValue === 'number') this.#mapVisibilityOptionsDropdown.addOptionElement(createTextInputOption(option, option, defaultValue.toString(), 'number', (ev: any) => { this.#setVisibilityOption(option, ev); }, options)); else this.#mapVisibilityOptionsDropdown.addOptionElement(createTextInputOption(option, option, defaultValue, 'text', (ev: any) => { this.#setVisibilityOption(option, ev); }, options)); } setLayer(layerName: string) { if (this.#layer != null) this.removeLayer(this.#layer) 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, layer); }) this.#layer = new L.LayerGroup(layers); } else { this.#layer = new L.TileLayer(layerData.urlTemplate, layerData); } } this.#layer?.addTo(this); } getLayers() { return Object.keys(this.#mapLayers); } /* State machine */ setState(state: string) { this.#state = state; this.#updateCursor(); /* 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 === COALITIONAREA_DRAW_POLYGON) { this.#coalitionAreas.push(new CoalitionArea([])); this.#coalitionAreas[this.#coalitionAreas.length - 1].addTo(this); } document.dispatchEvent(new CustomEvent("mapStateChanged")); } 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) { if (value) { if (this.#hiddenTypes.includes(key)) delete this.#hiddenTypes[this.#hiddenTypes.indexOf(key)]; } else { this.#hiddenTypes.push(key); } } 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 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 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 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 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 this.#coalitionAreaContextMenu; } hideCoalitionAreaContextMenu() { this.#coalitionAreaContextMenu.hide(); } getMousePosition() { return this.#lastMousePosition; } getMouseCoordinates() { return this.containerPointToLatLng(this.#lastMousePosition); } centerOnUnit(ID: number | null) { if (ID != null) { this.options.scrollWheelZoom = 'center'; this.#centerUnit = getApp().getUnitsManager().getUnitByID(ID); } 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]); } 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); } getVisibilityOptions() { return this.#visibilityOptions; } 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()); }); } getMapMarkerVisibilityControls() { return this.#mapMarkerVisibilityControls; } setSlaveDCSCamera(newSlaveDCSCamera: boolean) { // if (this.#slaveDCSCameraAvailable || !newSlaveDCSCamera) { // Commented to experiment with usability this.#slaveDCSCamera = newSlaveDCSCamera; let button = document.getElementById("camera-link-control"); button?.classList.toggle("off", !newSlaveDCSCamera); if (newSlaveDCSCamera) this.#broadcastPosition(); // } } setCameraControlMode(newCameraControlMode: string) { this.#cameraControlMode = newCameraControlMode; if (this.#slaveDCSCamera) this.#broadcastPosition(); } /* Event handlers */ #onClick(e: any) { if (!this.#preventLeftClick) { this.hideAllContextMenus(); if (this.#state === IDLE) { this.deselectAllCoalitionAreas(); } 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 = 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 { 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.#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 === 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() { getGroundElevation(this.getCenter(), (response: string) => { var groundElevation: number | null = null; try { groundElevation = parseFloat(response); var xmlHttp = new XMLHttpRequest(); xmlHttp.open("PUT", `http://127.0.0.1:${this.#cameraControlPort}`); xmlHttp.setRequestHeader("Content-Type", "application/json"); 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)); if (alt > 100000) alt = 100000; xmlHttp.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; } #createUnitMarkerControlButtons() { const unitVisibilityControls = document.getElementById("unit-visibility-control"); const makeTitle = (isProtected: boolean) => { return (isProtected) ? "Unit type is protected and will ignore orders" : "Unit is NOT protected and will respond to orders"; } this.getMapMarkerVisibilityControls().forEach((control: MapMarkerVisibilityControl) => { const toggles = `["${control.toggles.join('","')}"]`; const div = document.createElement("div"); div.className = control.protectable === true ? "protectable" : ""; // TODO: for consistency let's avoid using innerHTML. Let's create elements. div.innerHTML = ` `; unitVisibilityControls.appendChild(div); if (control.protectable) { div.innerHTML += ` `; const btn = div.querySelector("button.lock"); btn.addEventListener("click", (ev: MouseEventInit) => { control.isProtected = !control.isProtected; btn.toggleAttribute("data-protected", control.isProtected); btn.title = makeTitle(control.isProtected); document.dispatchEvent(new CustomEvent("toggleMarkerProtection", { detail: { "_element": btn, "control": control } })); }); } }); unitVisibilityControls.querySelectorAll(`img[src$=".svg"]`).forEach(img => SVGInjector(img)); } #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.#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 !== 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 === 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); } #setVisibilityOption(option: string, ev: any) { if (typeof this.#visibilityOptions[option] === 'boolean') this.#visibilityOptions[option] = ev.currentTarget.checked; else if (typeof this.#visibilityOptions[option] === 'number') this.#visibilityOptions[option] = Number(ev.currentTarget.value); else this.#visibilityOptions[option] = ev.currentTarget.value; document.dispatchEvent(new CustomEvent("mapOptionsChanged")); } #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"; } } } #checkCameraPort(){ var xmlHttp = new XMLHttpRequest(); xmlHttp.open("OPTIONS", `http://127.0.0.1:${this.#cameraControlPort}`); xmlHttp.onload = (res: any) => { if (xmlHttp.status == 200) this.#setSlaveDCSCameraAvailable(true); else this.#setSlaveDCSCameraAvailable(false); }; xmlHttp.onerror = (res: any) => { this.#setSlaveDCSCameraAvailable(false); } xmlHttp.ontimeout = (res: any) => { this.#setSlaveDCSCameraAvailable(false); } xmlHttp.timeout = 500; xmlHttp.send(""); } }