From d72a00a005f16467071cfe3d81f4d7f026b0d19c Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Wed, 3 Apr 2024 08:05:36 +0200 Subject: [PATCH] Started readding old core frontend code --- frontend/react/index.html | 2 +- frontend/react/package.json | 8 +- frontend/react/src/dom.d.ts | 39 + frontend/react/src/index.css | 1 - frontend/react/src/main.jsx | 10 - frontend/react/src/main.tsx | 14 + frontend/react/src/map/boxselect.ts | 136 + frontend/react/src/map/clickableminimap.ts | 12 + .../src/map/coalitionarea/coalitionarea.ts | 166 + .../map/coalitionarea/coalitionareahandle.ts | 19 + .../coalitionareamiddlehandle.ts | 19 + .../src/map/coalitionarea/drawingcursor.ts | 20 + frontend/react/src/map/dcslayer.ts | 49 + frontend/react/src/map/map.ts | 1075 ++- .../react/src/map/markers/custommarker.ts | 25 + .../map/markers/destinationpreviewHandle.ts | 19 + .../map/markers/destinationpreviewmarker.ts | 20 + frontend/react/src/map/markers/smokemarker.ts | 31 + .../react/src/map/markers/targetmarker.ts | 20 + .../src/map/markers/temporaryunitmarker.ts | 76 + frontend/react/src/map/rangecircle.ts | 56 + frontend/react/src/map/touchboxselect.ts | 136 + frontend/react/src/mission/airbase.ts | 96 + frontend/react/src/mission/bullseye.ts | 36 + frontend/react/src/mission/missionmanager.ts | 326 + frontend/react/src/olympusapp.ts | 502 ++ frontend/react/src/other/eventsmanager.ts | 7 + frontend/react/src/other/manager.ts | 37 + frontend/react/src/other/utils.ts | 476 ++ frontend/react/src/server/dataextractor.ts | 164 + frontend/react/src/server/servermanager.ts | 604 ++ frontend/react/src/shortcut/shortcut.ts | 48 + .../react/src/shortcut/shortcutmanager.ts | 65 + frontend/react/src/statecontext.tsx | 2 +- frontend/react/src/{App.css => ui.css} | 0 frontend/react/src/{App.tsx => ui.tsx} | 23 +- .../statebutton.tsx} | 2 +- .../src/ui/panels/components/menutitle.tsx | 9 + frontend/react/src/ui/{ => panels}/header.tsx | 8 +- frontend/react/src/ui/panels/spawnmenu.tsx | 14 + frontend/react/src/unit/contextaction.ts | 60 + frontend/react/src/unit/contextactionset.ts | 25 + .../src/unit/databases/aircraftdatabase.ts | 37 + .../src/unit/databases/citiesdatabase.ts | 7137 +++++++++++++++++ .../src/unit/databases/groundunitdatabase.ts | 36 + .../src/unit/databases/helicopterdatabase.ts | 37 + .../src/unit/databases/navyunitdatabase.ts | 36 + .../react/src/unit/databases/unitdatabase.ts | 239 + frontend/react/src/unit/group.ts | 45 + .../src/unit/importexport/unitdatafile.ts | 66 + .../unit/importexport/unitdatafileexport.ts | 101 + .../unit/importexport/unitdatafileimport.ts | 150 + frontend/react/src/unit/unit.ts | 1729 ++++ frontend/react/src/unit/unitsmanager.ts | 1544 ++++ frontend/react/src/weapon/weapon.ts | 316 + frontend/react/src/weapon/weaponsmanager.ts | 109 + frontend/react/tailwind.config.js | 4 +- frontend/server/app.js | 2 + frontend/server/demo.js | 4 +- frontend/server/package.json | 3 + 60 files changed, 16018 insertions(+), 34 deletions(-) create mode 100644 frontend/react/src/dom.d.ts delete mode 100644 frontend/react/src/main.jsx create mode 100644 frontend/react/src/main.tsx create mode 100644 frontend/react/src/map/boxselect.ts create mode 100644 frontend/react/src/map/clickableminimap.ts create mode 100644 frontend/react/src/map/coalitionarea/coalitionarea.ts create mode 100644 frontend/react/src/map/coalitionarea/coalitionareahandle.ts create mode 100644 frontend/react/src/map/coalitionarea/coalitionareamiddlehandle.ts create mode 100644 frontend/react/src/map/coalitionarea/drawingcursor.ts create mode 100644 frontend/react/src/map/dcslayer.ts create mode 100644 frontend/react/src/map/markers/custommarker.ts create mode 100644 frontend/react/src/map/markers/destinationpreviewHandle.ts create mode 100644 frontend/react/src/map/markers/destinationpreviewmarker.ts create mode 100644 frontend/react/src/map/markers/smokemarker.ts create mode 100644 frontend/react/src/map/markers/targetmarker.ts create mode 100644 frontend/react/src/map/markers/temporaryunitmarker.ts create mode 100644 frontend/react/src/map/rangecircle.ts create mode 100644 frontend/react/src/map/touchboxselect.ts create mode 100644 frontend/react/src/mission/airbase.ts create mode 100644 frontend/react/src/mission/bullseye.ts create mode 100644 frontend/react/src/mission/missionmanager.ts create mode 100644 frontend/react/src/olympusapp.ts create mode 100644 frontend/react/src/other/eventsmanager.ts create mode 100644 frontend/react/src/other/manager.ts create mode 100644 frontend/react/src/other/utils.ts create mode 100644 frontend/react/src/server/dataextractor.ts create mode 100644 frontend/react/src/server/servermanager.ts create mode 100644 frontend/react/src/shortcut/shortcut.ts create mode 100644 frontend/react/src/shortcut/shortcutmanager.ts rename frontend/react/src/{App.css => ui.css} (100%) rename frontend/react/src/{App.tsx => ui.tsx} (87%) rename frontend/react/src/ui/{statebuttons.tsx => buttons/statebutton.tsx} (93%) create mode 100644 frontend/react/src/ui/panels/components/menutitle.tsx rename frontend/react/src/ui/{ => panels}/header.tsx (80%) create mode 100644 frontend/react/src/ui/panels/spawnmenu.tsx create mode 100644 frontend/react/src/unit/contextaction.ts create mode 100644 frontend/react/src/unit/contextactionset.ts create mode 100644 frontend/react/src/unit/databases/aircraftdatabase.ts create mode 100644 frontend/react/src/unit/databases/citiesdatabase.ts create mode 100644 frontend/react/src/unit/databases/groundunitdatabase.ts create mode 100644 frontend/react/src/unit/databases/helicopterdatabase.ts create mode 100644 frontend/react/src/unit/databases/navyunitdatabase.ts create mode 100644 frontend/react/src/unit/databases/unitdatabase.ts create mode 100644 frontend/react/src/unit/group.ts create mode 100644 frontend/react/src/unit/importexport/unitdatafile.ts create mode 100644 frontend/react/src/unit/importexport/unitdatafileexport.ts create mode 100644 frontend/react/src/unit/importexport/unitdatafileimport.ts create mode 100644 frontend/react/src/unit/unit.ts create mode 100644 frontend/react/src/unit/unitsmanager.ts create mode 100644 frontend/react/src/weapon/weapon.ts create mode 100644 frontend/react/src/weapon/weaponsmanager.ts diff --git a/frontend/react/index.html b/frontend/react/index.html index 0f9805e1..6beead09 100644 --- a/frontend/react/index.html +++ b/frontend/react/index.html @@ -8,6 +8,6 @@
- + diff --git a/frontend/react/package.json b/frontend/react/package.json index fb83550c..63ec1769 100644 --- a/frontend/react/package.json +++ b/frontend/react/package.json @@ -13,13 +13,19 @@ "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", + "@tanem/svg-injector": "^10.1.68", + "@turf/turf": "^6.5.0", "@types/leaflet": "^1.9.8", "@types/react-leaflet": "^3.0.0", + "@types/turf": "^3.5.32", + "js-sha256": "^0.11.0", "leaflet": "^1.9.4", "leaflet-control-mini-map": "^0.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-leaflet": "^4.2.1" + "react-leaflet": "^4.2.1", + "turf": "^3.0.14", + "usng": "^0.3.0" }, "devDependencies": { "@types/react": "^18.2.66", diff --git a/frontend/react/src/dom.d.ts b/frontend/react/src/dom.d.ts new file mode 100644 index 00000000..974e40f5 --- /dev/null +++ b/frontend/react/src/dom.d.ts @@ -0,0 +1,39 @@ +import { Unit } from "./unit/unit"; + +interface CustomEventMap { + "unitSelection": CustomEvent, + "unitDeselection": CustomEvent, + "unitsSelection": CustomEvent, + "unitsDeselection": CustomEvent, + "clearSelection": CustomEvent, + "unitCreation": CustomEvent, + "unitDeletion": CustomEvent, + "unitDeath": CustomEvent, + "unitUpdated": CustomEvent, + "unitMoveCommand": CustomEvent, + "unitAttackCommand": CustomEvent, + "unitLandCommand": CustomEvent, + "unitSetAltitudeCommand": CustomEvent, + "unitSetSpeedCommand": CustomEvent, + "unitSetOption": CustomEvent, + "groupCreation": CustomEvent, + "groupDeletion": CustomEvent, + "mapStateChanged": CustomEvent, + "mapContextMenu": CustomEvent, + "mapOptionsChanged": CustomEvent, + "commandModeOptionsChanged": CustomEvent, + "contactsUpdated": CustomEvent, + "activeCoalitionChanged": CustomEvent +} + +declare global { + interface Document { + addEventListener(type: K, + listener: (this: Document, ev: CustomEventMap[K]) => void): void; + dispatchEvent(ev: CustomEventMap[K]): void; + } + + //function getOlympusPlugin(): OlympusPlugin; +} + +export { }; diff --git a/frontend/react/src/index.css b/frontend/react/src/index.css index 812d1cf2..8747408b 100644 --- a/frontend/react/src/index.css +++ b/frontend/react/src/index.css @@ -1,7 +1,6 @@ @import "../node_modules/leaflet/dist/leaflet.css"; :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; diff --git a/frontend/react/src/main.jsx b/frontend/react/src/main.jsx deleted file mode 100644 index d6041272..00000000 --- a/frontend/react/src/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './app.tsx' -import './index.css' - -ReactDOM.createRoot(document.getElementById('root')).render( - - - , -) diff --git a/frontend/react/src/main.tsx b/frontend/react/src/main.tsx new file mode 100644 index 00000000..085533b3 --- /dev/null +++ b/frontend/react/src/main.tsx @@ -0,0 +1,14 @@ +/***************** UI *******************/ +import React from 'react' +import ReactDOM from 'react-dom/client' +import UI from './ui.js' +import './index.css' +import { setupApp } from './olympusapp.js' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + , +) + +window.onload = setupApp; diff --git a/frontend/react/src/map/boxselect.ts b/frontend/react/src/map/boxselect.ts new file mode 100644 index 00000000..c7269174 --- /dev/null +++ b/frontend/react/src/map/boxselect.ts @@ -0,0 +1,136 @@ +import { Map } from 'leaflet'; +import { Handler } from 'leaflet'; +import { Util } from 'leaflet'; +import { DomUtil } from 'leaflet'; +import { DomEvent } from 'leaflet'; +import { LatLngBounds } from 'leaflet'; +import { Bounds } from 'leaflet'; + +export var BoxSelect = Handler.extend({ + initialize: function (map) { + this._map = map; + this._container = map.getContainer(); + this._pane = map.getPanes().overlayPane; + this._resetStateTimeout = 0; + map.on('unload', this._destroy, this); + }, + + addHooks: function () { + DomEvent.on(this._container, 'mousedown', this._onMouseDown, this); + }, + + removeHooks: function () { + DomEvent.off(this._container, 'mousedown', this._onMouseDown, this); + }, + + moved: function () { + return this._moved; + }, + + _destroy: function () { + DomUtil.remove(this._pane); + delete this._pane; + }, + + _resetState: function () { + this._resetStateTimeout = 0; + this._moved = false; + }, + + _clearDeferredResetState: function () { + if (this._resetStateTimeout !== 0) { + clearTimeout(this._resetStateTimeout); + this._resetStateTimeout = 0; + } + }, + + _onMouseDown: function (e: any) { + if ((e.which == 1 && e.button == 0 && e.shiftKey)) { + this._map.fire('selectionstart'); + // Clear the deferred resetState if it hasn't executed yet, otherwise it + // will interrupt the interaction and orphan a box element in the container. + this._clearDeferredResetState(); + this._resetState(); + + DomUtil.disableTextSelection(); + DomUtil.disableImageDrag(); + + this._startPoint = this._map.mouseEventToContainerPoint(e); + + //@ts-ignore + DomEvent.on(document, { + contextmenu: DomEvent.stop, + mousemove: this._onMouseMove, + mouseup: this._onMouseUp, + keydown: this._onKeyDown + }, this); + } else { + return false; + } + }, + + _onMouseMove: function (e: any) { + if (!this._moved) { + this._moved = true; + + this._box = DomUtil.create('div', 'leaflet-zoom-box', this._container); + DomUtil.addClass(this._container, 'leaflet-crosshair'); + + this._map.fire('boxzoomstart'); + } + + this._point = this._map.mouseEventToContainerPoint(e); + + var bounds = new Bounds(this._point, this._startPoint), + size = bounds.getSize(); + + if (bounds.min != undefined) + DomUtil.setPosition(this._box, bounds.min); + + this._box.style.width = size.x + 'px'; + this._box.style.height = size.y + 'px'; + }, + + _finish: function () { + if (this._moved) { + DomUtil.remove(this._box); + DomUtil.removeClass(this._container, 'leaflet-crosshair'); + } + + DomUtil.enableTextSelection(); + DomUtil.enableImageDrag(); + + //@ts-ignore + DomEvent.off(document, { + contextmenu: DomEvent.stop, + mousemove: this._onMouseMove, + mouseup: this._onMouseUp, + keydown: this._onKeyDown + }, this); + }, + + _onMouseUp: function (e: any) { + if ((e.which !== 1) && (e.button !== 0)) { return; } + + this._finish(); + + if (!this._moved) { return; } + // Postpone to next JS tick so internal click event handling + // still see it as "moved". + window.setTimeout(Util.bind(this._resetState, this), 0); + var bounds = new LatLngBounds( + this._map.containerPointToLatLng(this._startPoint), + this._map.containerPointToLatLng(this._point)); + + this._map.fire('selectionend', { selectionBounds: bounds }); + }, + + _onKeyDown: function (e: any) { + if (e.keyCode === 27) { + this._finish(); + this._clearDeferredResetState(); + this._resetState(); + } + } +}); + diff --git a/frontend/react/src/map/clickableminimap.ts b/frontend/react/src/map/clickableminimap.ts new file mode 100644 index 00000000..00104f7e --- /dev/null +++ b/frontend/react/src/map/clickableminimap.ts @@ -0,0 +1,12 @@ +import { MiniMap, MiniMapOptions } from "leaflet-control-mini-map"; + +export class ClickableMiniMap extends MiniMap { + constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) { + super(layer, options); + } + + getMap() { + //@ts-ignore needed to access not exported member. A bit of a hack, required to access click events //TODO: fix me + return this._miniMap; + } +} \ No newline at end of file diff --git a/frontend/react/src/map/coalitionarea/coalitionarea.ts b/frontend/react/src/map/coalitionarea/coalitionarea.ts new file mode 100644 index 00000000..04ddcdc1 --- /dev/null +++ b/frontend/react/src/map/coalitionarea/coalitionarea.ts @@ -0,0 +1,166 @@ +import { DomUtil, LatLng, LatLngExpression, Map, Point, Polygon, PolylineOptions } from "leaflet"; +import { getApp } from "../../olympusapp"; +import { CoalitionAreaHandle } from "./coalitionareahandle"; +import { CoalitionAreaMiddleHandle } from "./coalitionareamiddlehandle"; +import { BLUE_COMMANDER, RED_COMMANDER } from "../../constants/constants"; + +export class CoalitionArea extends Polygon { + #coalition: string = "blue"; + #selected: boolean = true; + #editing: boolean = true; + #handles: CoalitionAreaHandle[] = []; + #middleHandles: CoalitionAreaMiddleHandle[] = []; + #activeIndex: number = 0; + + constructor(latlngs: LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][], options?: PolylineOptions) { + if (options === undefined) + options = {}; + + options.bubblingMouseEvents = false; + options.interactive = false; + + super(latlngs, options); + this.#setColors(); + this.#registerCallbacks(); + + if ([BLUE_COMMANDER, RED_COMMANDER].includes(getApp().getMissionManager().getCommandModeOptions().commandMode)) + this.setCoalition(getApp().getMissionManager().getCommandedCoalition()); + } + + setCoalition(coalition: string) { + this.#coalition = coalition; + this.#setColors(); + } + + getCoalition() { + return this.#coalition; + } + + setSelected(selected: boolean) { + this.#selected = selected; + this.#setColors(); + this.#setHandles(); + this.setOpacity(selected? 1: 0.5); + if (!this.getSelected() && this.getEditing()) { + /* Remove the vertex we were working on */ + var latlngs = this.getLatLngs()[0] as LatLng[]; + latlngs.splice(this.#activeIndex, 1); + this.setLatLngs(latlngs); + this.setEditing(false); + } + } + + getSelected() { + return this.#selected; + } + + setEditing(editing: boolean) { + this.#editing = editing; + this.#setHandles(); + var latlngs = this.getLatLngs()[0] as LatLng[]; + + /* Remove areas with less than 2 vertexes */ + if (latlngs.length <= 2) + getApp().getMap().deleteCoalitionArea(this); + } + + getEditing() { + return this.#editing; + } + + addTemporaryLatLng(latlng: LatLng) { + this.#activeIndex++; + var latlngs = this.getLatLngs()[0] as LatLng[]; + latlngs.splice(this.#activeIndex, 0, latlng); + this.setLatLngs(latlngs); + this.#setHandles(); + } + + moveActiveVertex(latlng: LatLng) { + var latlngs = this.getLatLngs()[0] as LatLng[]; + latlngs[this.#activeIndex] = latlng; + this.setLatLngs(latlngs); + this.#setHandles(); + } + + setOpacity(opacity: number) { + this.setStyle({opacity: opacity, fillOpacity: opacity * 0.25}); + } + + onRemove(map: Map): this { + super.onRemove(map); + this.#handles.concat(this.#middleHandles).forEach((handle: CoalitionAreaHandle | CoalitionAreaMiddleHandle) => handle.removeFrom(getApp().getMap())); + return this; + } + + #setColors() { + const coalitionColor = this.getCoalition() === "blue" ? "#247be2" : "#ff5858"; + this.setStyle({ color: this.getSelected() ? "white" : coalitionColor, fillColor: coalitionColor }); + } + + #setHandles() { + this.#handles.forEach((handle: CoalitionAreaHandle) => handle.removeFrom(getApp().getMap())); + this.#handles = []; + if (this.getSelected()) { + var latlngs = this.getLatLngs()[0] as LatLng[]; + latlngs.forEach((latlng: LatLng, idx: number) => { + /* Add the polygon vertex handle (for moving the vertex) */ + const handle = new CoalitionAreaHandle(latlng); + handle.addTo(getApp().getMap()); + handle.on("drag", (e: any) => { + var latlngs = this.getLatLngs()[0] as LatLng[]; + latlngs[idx] = e.target.getLatLng(); + this.setLatLngs(latlngs); + this.#setMiddleHandles(); + }); + this.#handles.push(handle); + }); + } + this.#setMiddleHandles(); + } + + #setMiddleHandles() { + this.#middleHandles.forEach((handle: CoalitionAreaMiddleHandle) => handle.removeFrom(getApp().getMap())); + this.#middleHandles = []; + var latlngs = this.getLatLngs()[0] as LatLng[]; + if (this.getSelected() && latlngs.length >= 2) { + var lastLatLng: LatLng | null = null; + latlngs.concat([latlngs[0]]).forEach((latlng: LatLng, idx: number) => { + /* Add the polygon middle point handle (for adding new vertexes) */ + if (lastLatLng != null) { + const handle1Point = getApp().getMap().latLngToLayerPoint(latlng); + const handle2Point = getApp().getMap().latLngToLayerPoint(lastLatLng); + const middlePoint = new Point((handle1Point.x + handle2Point.x) / 2, (handle1Point.y + handle2Point.y) / 2); + const middleLatLng = getApp().getMap().layerPointToLatLng(middlePoint); + + const middleHandle = new CoalitionAreaMiddleHandle(middleLatLng); + middleHandle.addTo(getApp().getMap()); + middleHandle.on("click", (e: any) => { + this.#activeIndex = idx - 1; + this.addTemporaryLatLng(middleLatLng); + }); + this.#middleHandles.push(middleHandle); + } + lastLatLng = latlng; + }); + } + } + + #registerCallbacks() { + this.on("click", (e: any) => { + getApp().getMap().deselectAllCoalitionAreas(); + if (!this.getSelected()) { + this.setSelected(true); + } + }); + + this.on("contextmenu", (e: any) => { + if (!this.getEditing()) { + getApp().getMap().deselectAllCoalitionAreas(); + this.setSelected(true); + } + else + this.setEditing(false); + }); + } +} \ No newline at end of file diff --git a/frontend/react/src/map/coalitionarea/coalitionareahandle.ts b/frontend/react/src/map/coalitionarea/coalitionareahandle.ts new file mode 100644 index 00000000..26421c85 --- /dev/null +++ b/frontend/react/src/map/coalitionarea/coalitionareahandle.ts @@ -0,0 +1,19 @@ +import { DivIcon, LatLng } from "leaflet"; +import { CustomMarker } from "../markers/custommarker"; + +export class CoalitionAreaHandle extends CustomMarker { + constructor(latlng: LatLng) { + super(latlng, {interactive: true, draggable: true}); + } + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [24, 24], + iconAnchor: [12, 12], + className: "leaflet-coalitionarea-handle-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-coalitionarea-handle-icon"); + this.getElement()?.appendChild(el); + } +} \ No newline at end of file diff --git a/frontend/react/src/map/coalitionarea/coalitionareamiddlehandle.ts b/frontend/react/src/map/coalitionarea/coalitionareamiddlehandle.ts new file mode 100644 index 00000000..06cd4a57 --- /dev/null +++ b/frontend/react/src/map/coalitionarea/coalitionareamiddlehandle.ts @@ -0,0 +1,19 @@ +import { DivIcon, LatLng } from "leaflet"; +import { CustomMarker } from "../markers/custommarker"; + +export class CoalitionAreaMiddleHandle extends CustomMarker { + constructor(latlng: LatLng) { + super(latlng, {interactive: true, draggable: false}); + } + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [16, 16], + iconAnchor: [8, 8], + className: "leaflet-coalitionarea-middle-handle-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-coalitionarea-middle-handle-icon"); + this.getElement()?.appendChild(el); + } +} \ No newline at end of file diff --git a/frontend/react/src/map/coalitionarea/drawingcursor.ts b/frontend/react/src/map/coalitionarea/drawingcursor.ts new file mode 100644 index 00000000..e4497952 --- /dev/null +++ b/frontend/react/src/map/coalitionarea/drawingcursor.ts @@ -0,0 +1,20 @@ +import { DivIcon, LatLng } from "leaflet"; +import { CustomMarker } from "../markers/custommarker"; + +export class DrawingCursor extends CustomMarker { + constructor() { + super(new LatLng(0, 0), {interactive: false}) + this.setZIndexOffset(9999); + } + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [24, 24], + iconAnchor: [0, 24], + className: "leaflet-draw-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-draw-icon"); + this.getElement()?.appendChild(el); + } +} \ No newline at end of file diff --git a/frontend/react/src/map/dcslayer.ts b/frontend/react/src/map/dcslayer.ts new file mode 100644 index 00000000..6b59b48f --- /dev/null +++ b/frontend/react/src/map/dcslayer.ts @@ -0,0 +1,49 @@ +import * as L from "leaflet" + +export class DCSLayer extends L.TileLayer { + createTile(coords: L.Coords, done: L.DoneCallback) { + let newDone = (error?: Error, tile?: HTMLElement) => { + if (error === null && tile !== undefined && !tile.classList.contains('filtered')) { + // Create a canvas and set its width and height. + var canvas = document.createElement('canvas'); + canvas.setAttribute('width', '256px'); + canvas.setAttribute('height', '256px'); + + // Get the canvas drawing context, and draw the image to it. + var context = canvas.getContext('2d'); + if (context) { + context.drawImage(tile as CanvasImageSource, 0, 0, canvas.width, canvas.height); + + // Get the canvas image data. + var imageData = context.getImageData(0, 0, canvas.width, canvas.height); + + // Create a function for preserving a specified colour. + var makeTransparent = function(imageData: ImageData, color: {r: number, g: number, b: number}) { + // Get the pixel data from the source. + var data = imageData.data; + // Iterate through all the pixels. + for (var i = 0; i < data.length; i += 4) { + // Check if the current pixel should have preserved transparency. This simply compares whether the color we passed in is equivalent to our pixel data. + var convert = data[i] > color.r - 5 && data[i] < color.r + 5 + && data[i + 1] > color.g - 5 && data[i + 1] < color.g + 5 + && data[i + 2] > color.b - 5 && data[i + 2] < color.b + 5; + + // Either preserve the initial transparency or set the transparency to 0. + data[i + 3] = convert ? 100: data[i + 3]; + } + return imageData; + }; + + // Get the new pixel data and set it to the canvas context. + var newData = makeTransparent(imageData, {r: 26, g: 109, b: 127}); + context.putImageData(newData, 0, 0); + (tile as HTMLImageElement).src = canvas.toDataURL(); + tile.classList.add('filtered'); + } + } else { + return done(error, tile); + } + } + return super.createTile(coords, newDone); + } +} \ No newline at end of file diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index d36a0d6b..6aa95d9c 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -1,6 +1,1075 @@ - 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, 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, DCSMapsZoomLevelsByTheatre, DCS_LINK_RATIO } 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"; +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 { - -} \ No newline at end of file + #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[] = []; + #layerName: string = ""; + #cameraOptionsXmlHttp: XMLHttpRequest | null = null; + #bradcastPositionXmlHttp: XMLHttpRequest | null = null; + #cameraZoomRatio: number = 1.0; + + /** + * + * @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"); + + /* 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('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; + this.#cameraZoomRatio = 50 / (20 + (this.getVisibilityOptions()[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) + + /* Option buttons */ + this.#createUnitMarkerControlButtons(); + + /* Create the checkboxes to select the advanced visibility options */ + this.addVisibilityOption(DCS_LINK_PORT, 3003, { min: 1024, max: 65535 }); + this.addVisibilityOption(DCS_LINK_RATIO, 50, { min: 0, max: 100, slider: true }); + + //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') { + // if (options !== undefined && options?.slider === true) + // this.#mapVisibilityOptionsDropdown.addOptionElement(createSliderInputOption(option, option, defaultValue, (ev: any) => { this.#setVisibilityOption(option, ev); }, options)); + // else + // 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) + + 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", "DCS Satellite"].includes(layerName) ) { + let layerData = this.#mapLayers["ArcGIS Satellite"]; + let layers = [new L.TileLayer(layerData.urlTemplate, layerData)]; + + let template = `https://maps.dcsolympus.com/maps/${layerName === "DCS Map"? "alt": "sat"}-{theatre}-modern/{z}/{x}/{y}.png`; + layers.push(...DCSMapsZoomLevelsByTheatre[theatre].map((nativeZoomLevels: any) => { + return new L.TileLayer(template.replace("{theatre}", theatre.toLowerCase()), {...nativeZoomLevels, maxZoom: 20, crossOrigin: ""}); + })); + + this.#layer = new L.LayerGroup(layers); + } + + this.#layer?.addTo(this); + this.#layerName = layerName; + } + + getLayers() { + let layers = ["DCS Map", "DCS Satellite"] + layers.push(...Object.keys(this.#mapLayers)); + return layers; + } + + /* 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 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.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]); + + 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); + } + + 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()); + //}); + 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 === 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 { + 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() { + 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; + } + + #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"; + } + } + } + + /* 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/map/markers/custommarker.ts b/frontend/react/src/map/markers/custommarker.ts new file mode 100644 index 00000000..a6ca5998 --- /dev/null +++ b/frontend/react/src/map/markers/custommarker.ts @@ -0,0 +1,25 @@ +import { DivIcon, Map, Marker } from "leaflet"; +import { MarkerOptions } from "leaflet"; +import { LatLngExpression } from "leaflet"; + +export class CustomMarker extends Marker { + constructor(latlng: LatLngExpression, options?: MarkerOptions) { + super(latlng, options); + } + + onAdd(map: Map): this { + this.setIcon(new DivIcon()); // Default empty icon + super.onAdd(map); + this.createIcon(); + return this; + } + + onRemove(map: Map): this { + super.onRemove(map); + return this; + } + + createIcon() { + /* Overloaded by child classes */ + } +} \ No newline at end of file diff --git a/frontend/react/src/map/markers/destinationpreviewHandle.ts b/frontend/react/src/map/markers/destinationpreviewHandle.ts new file mode 100644 index 00000000..e6037edf --- /dev/null +++ b/frontend/react/src/map/markers/destinationpreviewHandle.ts @@ -0,0 +1,19 @@ +import { DivIcon, LatLng } from "leaflet"; +import { CustomMarker } from "../markers/custommarker"; + +export class DestinationPreviewHandle extends CustomMarker { + constructor(latlng: LatLng) { + super(latlng, {interactive: true, draggable: true}); + } + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [18, 18], + iconAnchor: [9, 9], + className: "leaflet-destination-preview-handle-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-destination-preview-handle-icon"); + this.getElement()?.appendChild(el); + } +} \ No newline at end of file diff --git a/frontend/react/src/map/markers/destinationpreviewmarker.ts b/frontend/react/src/map/markers/destinationpreviewmarker.ts new file mode 100644 index 00000000..28f21650 --- /dev/null +++ b/frontend/react/src/map/markers/destinationpreviewmarker.ts @@ -0,0 +1,20 @@ +import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet"; +import { CustomMarker } from "./custommarker"; + +export class DestinationPreviewMarker extends CustomMarker { + constructor(latlng: LatLngExpression, options?: MarkerOptions) { + super(latlng, options); + this.setZIndexOffset(9999); + } + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [52, 52], + iconAnchor: [26, 26], + className: "leaflet-destination-preview", + })); + var el = document.createElement("div"); + el.classList.add("ol-destination-preview-icon"); + this.getElement()?.appendChild(el); + } +} diff --git a/frontend/react/src/map/markers/smokemarker.ts b/frontend/react/src/map/markers/smokemarker.ts new file mode 100644 index 00000000..7b932d94 --- /dev/null +++ b/frontend/react/src/map/markers/smokemarker.ts @@ -0,0 +1,31 @@ +import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet"; +import { CustomMarker } from "./custommarker"; +import { SVGInjector } from "@tanem/svg-injector"; +import { getApp } from "../../olympusapp"; + +export class SmokeMarker extends CustomMarker { + #color: string; + + constructor(latlng: LatLngExpression, color: string, options?: MarkerOptions) { + super(latlng, options); + this.setZIndexOffset(9999); + this.#color = color; + window.setTimeout(() => { this.removeFrom(getApp().getMap()); }, 300000) /* Remove the smoke after 5 minutes */ + } + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [52, 52], + iconAnchor: [26, 52], + className: "leaflet-smoke-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-smoke-icon"); + el.setAttribute("data-color", this.#color); + var img = document.createElement("img"); + img.src = "/resources/theme/images/markers/smoke.svg"; + img.onload = () => SVGInjector(img); + el.appendChild(img); + this.getElement()?.appendChild(el); + } +} diff --git a/frontend/react/src/map/markers/targetmarker.ts b/frontend/react/src/map/markers/targetmarker.ts new file mode 100644 index 00000000..9b781f1c --- /dev/null +++ b/frontend/react/src/map/markers/targetmarker.ts @@ -0,0 +1,20 @@ +import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet"; +import { CustomMarker } from "./custommarker"; + +export class TargetMarker extends CustomMarker { + constructor(latlng: LatLngExpression, options?: MarkerOptions) { + super(latlng, options); + this.setZIndexOffset(9999); + } + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [52, 52], + iconAnchor: [26, 26], + className: "leaflet-target-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-target-icon"); + this.getElement()?.appendChild(el); + } +} diff --git a/frontend/react/src/map/markers/temporaryunitmarker.ts b/frontend/react/src/map/markers/temporaryunitmarker.ts new file mode 100644 index 00000000..eeb77927 --- /dev/null +++ b/frontend/react/src/map/markers/temporaryunitmarker.ts @@ -0,0 +1,76 @@ +import { CustomMarker } from "./custommarker"; +import { DivIcon, LatLng } from "leaflet"; +import { SVGInjector } from "@tanem/svg-injector"; +import { getMarkerCategoryByName, getUnitDatabaseByCategory } from "../../other/utils"; +import { getApp } from "../../olympusapp"; + +export class TemporaryUnitMarker extends CustomMarker { + #name: string; + #coalition: string; + #commandHash: string|undefined = undefined; + #timer: number = 0; + + constructor(latlng: LatLng, name: string, coalition: string, commandHash?: string) { + super(latlng, {interactive: false}); + this.#name = name; + this.#coalition = coalition; + this.#commandHash = commandHash; + + if (commandHash !== undefined) + this.setCommandHash(commandHash) + } + + setCommandHash(commandHash: string) { + this.#commandHash = commandHash; + this.#timer = window.setInterval(() => { + if (this.#commandHash !== undefined) { + getApp().getServerManager().isCommandExecuted((res: any) => { + if (res.commandExecuted) { + this.removeFrom(getApp().getMap()); + window.clearInterval(this.#timer); + } + }, this.#commandHash) + } + }, 1000); + } + + createIcon() { + const category = getMarkerCategoryByName(this.#name); + const databaseEntry = getUnitDatabaseByCategory(category)?.getByName(this.#name); + + /* Set the icon */ + var icon = new DivIcon({ + className: 'leaflet-unit-icon', + iconAnchor: [25, 25], + iconSize: [50, 50], + }); + this.setIcon(icon); + + var el = document.createElement("div"); + el.classList.add("unit"); + el.setAttribute("data-object", `unit-${category}`); + el.setAttribute("data-coalition", this.#coalition); + + // Main icon + var unitIcon = document.createElement("div"); + unitIcon.classList.add("unit-icon"); + var img = document.createElement("img"); + + img.src = `/resources/theme/images/units/${databaseEntry?.markerFile ?? category}.svg`; + img.onload = () => SVGInjector(img); + unitIcon.appendChild(img); + unitIcon.toggleAttribute("data-rotate-to-heading", false); + el.append(unitIcon); + + // Short label + if (category == "aircraft" || category == "helicopter") { + var shortLabel = document.createElement("div"); + shortLabel.classList.add("unit-short-label"); + shortLabel.innerText = databaseEntry?.shortLabel || ""; + el.append(shortLabel); + } + + this.getElement()?.appendChild(el); + this.getElement()?.classList.add("ol-temporary-marker"); + } +} \ No newline at end of file diff --git a/frontend/react/src/map/rangecircle.ts b/frontend/react/src/map/rangecircle.ts new file mode 100644 index 00000000..2fa5d9b1 --- /dev/null +++ b/frontend/react/src/map/rangecircle.ts @@ -0,0 +1,56 @@ +// @ts-nocheck +// This is a horrible hack. But it is needed at the moment to ovveride a default behaviour of Leaflet. TODO please fix me the proper way. + +import { Circle, Point, Polyline } from 'leaflet'; + +/** + * This custom Circle object implements a faster render method for very big circles. When zoomed in, the default ctx.arc method + * is very slow since the circle is huge. Also, when zoomed in most of the circle points will be outside the screen and not needed. This + * simpler, faster renderer approximates the circle with line segements and only draws those currently visibile. + * A more refined version using arcs could be implemented but this works good enough. + */ +export class RangeCircle extends Circle { + _updatePath() { + if (!this._renderer._drawing || this._empty()) { return; } + var p = this._point, + ctx = this._renderer._ctx, + r = Math.max(Math.round(this._radius), 1), + s = (Math.max(Math.round(this._radiusY), 1) || r) / r; + + if (s !== 1) { + ctx.save(); + ctx.scale(1, s); + } + + let pathBegun = false; + let dtheta = Math.PI * 2 / 120; + for (let theta = 0; theta <= Math.PI * 2; theta += dtheta) { + let p1 = new Point(p.x + r * Math.cos(theta), p.y / s + r * Math.sin(theta)); + let p2 = new Point(p.x + r * Math.cos(theta + dtheta), p.y / s + r * Math.sin(theta + dtheta)); + let l1 = this._map.layerPointToLatLng(p1); + let l2 = this._map.layerPointToLatLng(p2); + let line = new Polyline([l1, l2]); + if (this._map.getBounds().intersects(line.getBounds())) { + if (!pathBegun) { + ctx.beginPath(); + ctx.moveTo(p1.x, p1.y); + pathBegun = true; + } + ctx.lineTo(p2.x, p2.y); + } + else { + if (pathBegun) { + this._renderer._fillStroke(ctx, this); + pathBegun = false; + } + } + } + + if (pathBegun) + this._renderer._fillStroke(ctx, this); + + if (s !== 1) + ctx.restore(); + + } +} \ No newline at end of file diff --git a/frontend/react/src/map/touchboxselect.ts b/frontend/react/src/map/touchboxselect.ts new file mode 100644 index 00000000..f4a0cc20 --- /dev/null +++ b/frontend/react/src/map/touchboxselect.ts @@ -0,0 +1,136 @@ +import { Map, Point } from 'leaflet'; +import { Handler } from 'leaflet'; +import { Util } from 'leaflet'; +import { DomUtil } from 'leaflet'; +import { DomEvent } from 'leaflet'; +import { LatLngBounds } from 'leaflet'; +import { Bounds } from 'leaflet'; + +export var TouchBoxSelect = Handler.extend({ + initialize: function (map: Map) { + this._map = map; + this._container = map.getContainer(); + this._pane = map.getPanes().overlayPane; + this._resetStateTimeout = 0; + this._doubleClicked = false; + map.on('unload', this._destroy, this); + }, + + addHooks: function () { + DomEvent.on(this._container, 'touchstart', this._onMouseDown, this); + }, + + removeHooks: function () { + DomEvent.off(this._container, 'touchstart', this._onMouseDown, this); + }, + + moved: function () { + return this._moved; + }, + + _destroy: function () { + DomUtil.remove(this._pane); + delete this._pane; + }, + + _resetState: function () { + this._resetStateTimeout = 0; + this._moved = false; + }, + + _clearDeferredResetState: function () { + if (this._resetStateTimeout !== 0) { + clearTimeout(this._resetStateTimeout); + this._resetStateTimeout = 0; + } + }, + + _onMouseDown: function (e: any) { + if ((e.which == 0)) { + this._map.fire('selectionstart'); + // Clear the deferred resetState if it hasn't executed yet, otherwise it + // will interrupt the interaction and orphan a box element in the container. + this._clearDeferredResetState(); + this._resetState(); + + DomUtil.disableTextSelection(); + DomUtil.disableImageDrag(); + + this._startPoint = this._getMousePosition(e); + + //@ts-ignore + DomEvent.on(document, { + contextmenu: DomEvent.stop, + touchmove: this._onMouseMove, + touchend: this._onMouseUp + }, this); + } else { + return false; + } + }, + + _onMouseMove: function (e: any) { + if (!this._moved) { + this._moved = true; + + this._box = DomUtil.create('div', 'leaflet-zoom-box', this._container); + DomUtil.addClass(this._container, 'leaflet-crosshair'); + } + + this._point = this._getMousePosition(e); + + var bounds = new Bounds(this._point, this._startPoint), + size = bounds.getSize(); + + if (bounds.min != undefined) + DomUtil.setPosition(this._box, bounds.min); + + this._box.style.width = size.x + 'px'; + this._box.style.height = size.y + 'px'; + }, + + _finish: function () { + if (this._moved) { + DomUtil.remove(this._box); + DomUtil.removeClass(this._container, 'leaflet-crosshair'); + } + + DomUtil.enableTextSelection(); + DomUtil.enableImageDrag(); + + //@ts-ignore + DomEvent.off(document, { + contextmenu: DomEvent.stop, + touchmove: this._onMouseMove, + touchend: this._onMouseUp + }, this); + }, + + _onMouseUp: function (e: any) { + if ((e.which !== 0)) { return; } + + this._finish(); + + if (!this._moved) { return; } + // Postpone to next JS tick so internal click event handling + // still see it as "moved". + window.setTimeout(Util.bind(this._resetState, this), 0); + var bounds = new LatLngBounds( + this._map.containerPointToLatLng(this._startPoint), + this._map.containerPointToLatLng(this._point)); + + this._map.fire('selectionend', { selectionBounds: bounds }); + }, + + _getMousePosition(e: any) { + var scale = DomUtil.getScale(this._container), offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y) + + return new Point( + // offset.left/top values are in page scale (like clientX/Y), + // whereas clientLeft/Top (border width) values are the original values (before CSS scale applies). + (e.touches[0].clientX - offset.left) / scale.x - this._container.clientLeft, + (e.touches[0].clientY - offset.top) / scale.y - this._container.clientTop + ); + } +}); + diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts new file mode 100644 index 00000000..be3f7c55 --- /dev/null +++ b/frontend/react/src/mission/airbase.ts @@ -0,0 +1,96 @@ +import { DivIcon } from 'leaflet'; +import { CustomMarker } from '../map/markers/custommarker'; +import { SVGInjector } from '@tanem/svg-injector'; +import { AirbaseChartData, AirbaseOptions } from '../interfaces'; + + +export class Airbase extends CustomMarker { + #name: string = ""; + #chartData: AirbaseChartData = { + elevation: "", + ICAO: "", + TACAN: "", + runways: [] + }; + #coalition: string = ""; + #hasChartDataBeenSet: boolean = false; + #properties: string[] = []; + #parkings: string[] = []; + + constructor(options: AirbaseOptions) { + super(options.position, { riseOnHover: true }); + + this.#name = options.name; + } + + chartDataHasBeenSet() { + return this.#hasChartDataBeenSet; + } + + createIcon() { + var icon = new DivIcon({ + className: 'leaflet-airbase-marker', + iconSize: [40, 40], + iconAnchor: [20, 20] + }); // Set the marker, className must be set to avoid white square + this.setIcon(icon); + + var el = document.createElement("div"); + el.classList.add("airbase-icon"); + el.setAttribute("data-object", "airbase"); + var img = document.createElement("img"); + img.src = "/resources/theme/images/markers/airbase.svg"; + img.onload = () => SVGInjector(img); + el.appendChild(img); + this.getElement()?.appendChild(el); + el.addEventListener( "mouseover", ( ev ) => { + document.dispatchEvent( new CustomEvent( "airbaseMouseover", { detail: this })); + }); + el.addEventListener( "mouseout", ( ev ) => { + document.dispatchEvent( new CustomEvent( "airbaseMouseout", { detail: this })); + }); + el.dataset.coalition = this.#coalition; + } + + setCoalition(coalition: string) { + this.#coalition = coalition; + (this.getElement()?.querySelector(".airbase-icon")).dataset.coalition = this.#coalition; + } + + getChartData() { + return this.#chartData; + } + + getCoalition() { + return this.#coalition; + } + + setName(name: string) { + this.#name = name; + } + + getName() { + return this.#name; + } + + setChartData(chartData: AirbaseChartData) { + this.#hasChartDataBeenSet = true; + this.#chartData = chartData; + } + + setProperties(properties: string[]) { + this.#properties = properties; + } + + getProperties() { + return this.#properties; + } + + setParkings(parkings: string[]) { + this.#parkings = parkings; + } + + getParkings() { + return this.#parkings; + } +} \ No newline at end of file diff --git a/frontend/react/src/mission/bullseye.ts b/frontend/react/src/mission/bullseye.ts new file mode 100644 index 00000000..ccadfd77 --- /dev/null +++ b/frontend/react/src/mission/bullseye.ts @@ -0,0 +1,36 @@ +import { DivIcon } from "leaflet"; +import { CustomMarker } from "../map/markers/custommarker"; +import { SVGInjector } from "@tanem/svg-injector"; + +export class Bullseye extends CustomMarker { + #coalition: string = ""; + + createIcon() { + var icon = new DivIcon({ + className: 'leaflet-bullseye-marker', + iconSize: [40, 40], + iconAnchor: [20, 20] + }); // Set the marker, className must be set to avoid white square + this.setIcon(icon); + + var el = document.createElement("div"); + el.classList.add("bullseye-icon"); + el.setAttribute("data-object", "bullseye"); + var img = document.createElement("img"); + img.src = "/resources/theme/images/markers/bullseye.svg"; + img.onload = () => SVGInjector(img); + el.appendChild(img); + this.getElement()?.appendChild(el); + } + + setCoalition(coalition: string) + { + this.#coalition = coalition; + ( this.getElement()?.querySelector(".bullseye-icon")).dataset.coalition = this.#coalition; + } + + getCoalition() + { + return this.#coalition; + } +} \ No newline at end of file diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts new file mode 100644 index 00000000..6d6dcc72 --- /dev/null +++ b/frontend/react/src/mission/missionmanager.ts @@ -0,0 +1,326 @@ +import { LatLng } from "leaflet"; +import { getApp } from "../olympusapp"; +import { Airbase } from "./airbase"; +import { Bullseye } from "./bullseye"; +import { BLUE_COMMANDER, ERAS, GAME_MASTER, NONE, RED_COMMANDER } from "../constants/constants"; +//import { Dropdown } from "../controls/dropdown"; +import { groundUnitDatabase } from "../unit/databases/groundunitdatabase"; +//import { createCheckboxOption, getCheckboxOptions } from "../other/utils"; +import { aircraftDatabase } from "../unit/databases/aircraftdatabase"; +import { helicopterDatabase } from "../unit/databases/helicopterdatabase"; +import { navyUnitDatabase } from "../unit/databases/navyunitdatabase"; +//import { Popup } from "../popups/popup"; +import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData } from "../interfaces"; + +/** The MissionManager */ +export class MissionManager { + #bullseyes: { [name: string]: Bullseye } = {}; + #airbases: { [name: string]: Airbase } = {}; + #theatre: string = ""; + #dateAndTime: DateAndTime = {date: {Year: 0, Month: 0, Day: 0}, time: {h: 0, m: 0, s: 0}, startTime: 0, elapsedTime: 0}; + #commandModeOptions: CommandModeOptions = {commandMode: NONE, restrictSpawns: false, restrictToCoalition: false, setupTime: Infinity, spawnPoints: {red: Infinity, blue: Infinity}, eras: []}; + #remainingSetupTime: number = 0; + #spentSpawnPoint: number = 0; + //#commandModeDialog: HTMLElement; + //#commandModeErasDropdown: Dropdown; + #coalitions: {red: string[], blue: string[]} = {red: [], blue: []}; + + constructor() { + document.addEventListener("applycommandModeOptions", () => this.#applycommandModeOptions()); + document.addEventListener("showCommandModeDialog", () => this.showCommandModeDialog()); + document.addEventListener("toggleSpawnRestrictions", (ev:CustomEventInit) => { + this.#toggleSpawnRestrictions(ev.detail._element.checked) + }); + + /* command-mode settings dialog */ + //this.#commandModeDialog = document.querySelector("#command-mode-settings-dialog") as HTMLElement; + //this.#commandModeErasDropdown = new Dropdown("command-mode-era-options", () => {}); + + } + + /** Update location of bullseyes + * + * @param object + */ + updateBullseyes(data: BullseyesData) { + const commandMode = getApp().getMissionManager().getCommandModeOptions().commandMode; + for (let idx in data.bullseyes) { + const bullseye = data.bullseyes[idx]; + + // Prevent Red and Blue coalitions seeing each other's bulleye(s) + if ((bullseye.coalition === "red" && commandMode === BLUE_COMMANDER) + || (bullseye.coalition === "blue" && commandMode === RED_COMMANDER)) { + continue; + } + + if (!(idx in this.#bullseyes)) + this.#bullseyes[idx] = new Bullseye([0, 0]).addTo(getApp().getMap()); + + if (bullseye.latitude && bullseye.longitude && bullseye.coalition) { + this.#bullseyes[idx].setLatLng(new LatLng(bullseye.latitude, bullseye.longitude)); + this.#bullseyes[idx].setCoalition(bullseye.coalition); + } + } + } + + /** Update airbase information + * + * @param object + */ + updateAirbases(data: AirbasesData) { + for (let idx in data.airbases) { + var airbase = data.airbases[idx] + if (this.#airbases[airbase.callsign] === undefined && airbase.callsign != '') { + this.#airbases[airbase.callsign] = new Airbase({ + position: new LatLng(airbase.latitude, airbase.longitude), + name: airbase.callsign + }).addTo(getApp().getMap()); + this.#airbases[airbase.callsign].on('contextmenu', (e) => this.#onAirbaseClick(e)); + this.#loadAirbaseChartData(airbase.callsign); + } + + if (this.#airbases[airbase.callsign] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) { + this.#airbases[airbase.callsign].setLatLng(new LatLng(airbase.latitude, airbase.longitude)); + this.#airbases[airbase.callsign].setCoalition(airbase.coalition); + } + } + } + + /** Update mission information + * + * @param object + */ + updateMission(data: MissionData) { + if (data.mission) { + + /* Set the mission theatre */ + if (data.mission.theatre != this.#theatre) { + this.#theatre = data.mission.theatre; + getApp().getMap().setTheatre(this.#theatre); + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Map set to " + this.#theatre); + } + + /* Set the date and time data */ + this.#dateAndTime = data.mission.dateAndTime; + data.mission.dateAndTime.time.s -= 1; // ED has seconds 1-60 and not 0-59?! + + /* Set the coalition countries */ + this.#coalitions = data.mission.coalitions; + + /* Set the command mode options */ + this.#setcommandModeOptions(data.mission.commandModeOptions); + this.#remainingSetupTime = this.getCommandModeOptions().setupTime - this.getDateAndTime().elapsedTime; + var commandModePhaseEl = document.querySelector("#command-mode-phase") as HTMLElement; + if (commandModePhaseEl) { + if (this.#remainingSetupTime > 0) { + var remainingTime = `-${new Date(this.#remainingSetupTime * 1000).toISOString().substring(14, 19)}`; + commandModePhaseEl.dataset.remainingTime = remainingTime; + } + + commandModePhaseEl.classList.toggle("setup-phase", this.#remainingSetupTime > 0 && this.getCommandModeOptions().restrictSpawns); + //commandModePhaseEl.classList.toggle("game-commenced", this.#remainingSetupTime <= 0 || !this.getCommandModeOptions().restrictSpawns); + //commandModePhaseEl.classList.toggle("no-restrictions", !this.getCommandModeOptions().restrictSpawns); + } + } + } + + /** Get the bullseyes set in this theatre + * + * @returns object + */ + getBullseyes() { + return this.#bullseyes; + } + + /** Get the airbases in this theatre + * + * @returns object + */ + getAirbases() { + return this.#airbases; + } + + /** Get the options/settings as set in the command mode + * + * @returns object + */ + getCommandModeOptions() { + return this.#commandModeOptions; + } + + /** Get the current date and time of the mission (based on local time) + * + * @returns object + */ + getDateAndTime() { + return this.#dateAndTime; + } + + /** + * Get the number of seconds left of setup time + * @returns number + */ + getRemainingSetupTime() { + return this.#remainingSetupTime; + } + + /** Get an object with the coalitions in it + * + * @returns object + */ + getCoalitions() { + return this.#coalitions; + } + + /** Get the current theatre (map) name + * + * @returns string + */ + getTheatre() { + return this.#theatre; + } + + getAvailableSpawnPoints() { + if (this.getCommandModeOptions().commandMode === GAME_MASTER) + return Infinity; + else if (this.getCommandModeOptions().commandMode === BLUE_COMMANDER) + return this.getCommandModeOptions().spawnPoints.blue - this.#spentSpawnPoint; + else if (this.getCommandModeOptions().commandMode === RED_COMMANDER) + return this.getCommandModeOptions().spawnPoints.red - this.#spentSpawnPoint; + else + return 0; + } + + getCommandedCoalition() { + if (this.getCommandModeOptions().commandMode === BLUE_COMMANDER) + return "blue"; + else if (this.getCommandModeOptions().commandMode === RED_COMMANDER) + return "red"; + else + return "all"; + } + + refreshSpawnPoints() { + var spawnPointsEl = document.querySelector("#spawn-points"); + if (spawnPointsEl) { + spawnPointsEl.textContent = `${this.getAvailableSpawnPoints()}`; + } + } + + setSpentSpawnPoints(spawnPoints: number) { + this.#spentSpawnPoint = spawnPoints; + this.refreshSpawnPoints(); + } + + showCommandModeDialog() { + //const options = this.getCommandModeOptions() + //const { restrictSpawns, restrictToCoalition, setupTime } = options; + //this.#toggleSpawnRestrictions(restrictSpawns); +// + ///* Create the checkboxes to select the unit eras */ + //this.#commandModeErasDropdown.setOptionsElements( + // ERAS.sort((eraA, eraB) => { + // return ( eraA.chronologicalOrder > eraB.chronologicalOrder ) ? 1 : -1; + // }).map((era) => { + // return createCheckboxOption(era.name, `Enable ${era} units spawns`, this.getCommandModeOptions().eras.includes(era.name)); + // }) + //); +// + //this.#commandModeDialog.classList.remove("hide"); +// + //const restrictSpawnsCheckbox = this.#commandModeDialog.querySelector("#restrict-spawns")?.querySelector("input") as HTMLInputElement; + //const restrictToCoalitionCheckbox = this.#commandModeDialog.querySelector("#restrict-to-coalition")?.querySelector("input") as HTMLInputElement; + //const blueSpawnPointsInput = this.#commandModeDialog.querySelector("#blue-spawn-points")?.querySelector("input") as HTMLInputElement; + //const redSpawnPointsInput = this.#commandModeDialog.querySelector("#red-spawn-points")?.querySelector("input") as HTMLInputElement; + //const setupTimeInput = this.#commandModeDialog.querySelector("#setup-time")?.querySelector("input") as HTMLInputElement; +// + //restrictSpawnsCheckbox.checked = restrictSpawns; + //restrictToCoalitionCheckbox.checked = restrictToCoalition; + //blueSpawnPointsInput.value = String(options.spawnPoints.blue); + //redSpawnPointsInput.value = String(options.spawnPoints.red); + //setupTimeInput.value = String(Math.floor(setupTime / 60.0)); + } + + #applycommandModeOptions() { + //this.#commandModeDialog.classList.add("hide"); +// + //const restrictSpawnsCheckbox = this.#commandModeDialog.querySelector("#restrict-spawns")?.querySelector("input") as HTMLInputElement; + //const restrictToCoalitionCheckbox = this.#commandModeDialog.querySelector("#restrict-to-coalition")?.querySelector("input") as HTMLInputElement; + //const blueSpawnPointsInput = this.#commandModeDialog.querySelector("#blue-spawn-points")?.querySelector("input") as HTMLInputElement; + //const redSpawnPointsInput = this.#commandModeDialog.querySelector("#red-spawn-points")?.querySelector("input") as HTMLInputElement; + //const setupTimeInput = this.#commandModeDialog.querySelector("#setup-time")?.querySelector("input") as HTMLInputElement; +// + //var eras: string[] = []; + //const enabledEras = getCheckboxOptions(this.#commandModeErasDropdown); + //Object.keys(enabledEras).forEach((key: string) => {if (enabledEras[key]) eras.push(key)}); + //getApp().getServerManager().setCommandModeOptions(restrictSpawnsCheckbox.checked, restrictToCoalitionCheckbox.checked, {blue: parseInt(blueSpawnPointsInput.value), red: parseInt(redSpawnPointsInput.value)}, eras, parseInt(setupTimeInput.value) * 60); + } + + #setcommandModeOptions(commandModeOptions: CommandModeOptions) { + /* Refresh all the data if we have exited the NONE state */ + var requestRefresh = false; + if (this.#commandModeOptions.commandMode === NONE && commandModeOptions.commandMode !== NONE) + requestRefresh = true; + + /* Refresh the page if we have lost Game Master priviledges */ + if (this.#commandModeOptions.commandMode === GAME_MASTER && commandModeOptions.commandMode !== GAME_MASTER) + location.reload(); + + /* Check if any option has changed */ + var commandModeOptionsChanged = (!commandModeOptions.eras.every((value: string, idx: number) => {return value === this.getCommandModeOptions().eras[idx]}) || + commandModeOptions.spawnPoints.red !== this.getCommandModeOptions().spawnPoints.red || + commandModeOptions.spawnPoints.blue !== this.getCommandModeOptions().spawnPoints.blue || + commandModeOptions.restrictSpawns !== this.getCommandModeOptions().restrictSpawns || + commandModeOptions.restrictToCoalition !== this.getCommandModeOptions().restrictToCoalition); + + this.#commandModeOptions = commandModeOptions; + this.setSpentSpawnPoints(0); + this.refreshSpawnPoints(); + + if (commandModeOptionsChanged) { + document.dispatchEvent(new CustomEvent("commandModeOptionsChanged", { detail: this })); + document.getElementById("command-mode-toolbar")?.classList.remove("hide"); + const el = document.getElementById("command-mode"); + if (el) { + el.dataset.mode = commandModeOptions.commandMode; + el.textContent = commandModeOptions.commandMode.toUpperCase(); + } + } + + document.querySelector("#spawn-points-container")?.classList.toggle("hide", this.getCommandModeOptions().commandMode === GAME_MASTER || !this.getCommandModeOptions().restrictSpawns); + document.querySelector("#command-mode-settings-button")?.classList.toggle("hide", this.getCommandModeOptions().commandMode !== GAME_MASTER); + + if (requestRefresh) + getApp().getServerManager().refreshAll(); + } + + #onAirbaseClick(e: any) { + getApp().getMap().showAirbaseContextMenu(e.sourceTarget, e.originalEvent.x, e.originalEvent.y, e.latlng); + } + + #loadAirbaseChartData(callsign: string) { + if ( !this.#theatre ) { + return; + } + + var xhr = new XMLHttpRequest(); + xhr.open('GET', `api/airbases/${this.#theatre.toLowerCase()}/${callsign}`, true); + xhr.responseType = 'json'; + xhr.onload = () => { + var status = xhr.status; + if (status === 200) { + const data = xhr.response; + this.getAirbases()[callsign].setChartData(data); + } else { + console.error(`Error retrieving data for ${callsign} airbase`) + } + }; + xhr.send(); + } + + #toggleSpawnRestrictions(restrictionsEnabled:boolean) { + //this.#commandModeDialog.querySelectorAll("input, label, .ol-select").forEach( el => { + // if (!el.closest("#restrict-spawns")) el.toggleAttribute("disabled", !restrictionsEnabled); + //}); + } +} \ No newline at end of file diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts new file mode 100644 index 00000000..680af2cc --- /dev/null +++ b/frontend/react/src/olympusapp.ts @@ -0,0 +1,502 @@ +/***************** APP *******************/ +var app: OlympusApp; + +export function setupApp() { + app = new OlympusApp(); + app.start(); +} + +export function getApp() { + return app; +} + +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 { 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"; +var VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; + +export class OlympusApp { + /* Global data */ + #activeCoalition: string = "blue"; + #latestVersion: string|undefined = undefined; + #config: any = {}; + + /* Main leaflet map, extended by custom methods */ + #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; + #unitsManager: UnitsManager | null = null; + #weaponsManager: WeaponsManager | null = null; + + constructor() { + } + + // TODO add checks on null + getDialogManager() { + return null //this.#dialogManager as Manager; + } + + 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; + } + + getUnitsManager() { + return this.#unitsManager as UnitsManager; + } + + getWeaponsManager() { + return this.#weaponsManager as WeaponsManager; + } + + getMissionManager() { + return this.#missionManager as MissionManager; + } + + 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) { + if (this.getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER) { + this.#activeCoalition = newActiveCoalition; + document.dispatchEvent(new CustomEvent("activeCoalitionChanged")); + } + } + + /** + * + * @returns The active coalition + */ + getActiveCoalition() { + if (this.getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER) + return this.#activeCoalition; + else { + if (this.getMissionManager().getCommandModeOptions().commandMode == BLUE_COMMANDER) + return "blue"; + else if (this.getMissionManager().getCommandModeOptions().commandMode == RED_COMMANDER) + return "red"; + else + return "neutral"; + } + } + + /** + * + * @returns The aircraft database + */ + getAircraftDatabase() { + return aircraftDatabase; + } + + /** + * + * @returns The helicopter database + */ + getHelicopterDatabase() { + return helicopterDatabase; + } + + /** + * + * @returns The ground unit database + */ + getGroundUnitDatabase() { + return groundUnitDatabase; + } + + /** + * + * @returns The navy unit database + */ + getNavyUnitDatabase() { + 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) => { + if (response.status === 200) { + return response.json(); + } else { + throw new Error("Error connecting to Github to retrieve latest version"); + } + }).then((res) => { + this.#latestVersion = res["version"]; + const latestVersionSpan = document.getElementById("latest-version") as HTMLElement; + if (latestVersionSpan) { + latestVersionSpan.innerHTML = this.#latestVersion ?? "Unknown"; + latestVersionSpan.classList.toggle("new-version", this.#latestVersion !== VERSION); + } + }) + + /* Load the config file from the server */ + const configRequest = new Request("http://localhost:3000/" + "resources/config"); + fetch(configRequest).then((response) => { + if (response.status === 200) { + return response.json(); + } else { + throw new Error("Error retrieving config file"); + } + }).then((res) => { + this.#config = res; + document.dispatchEvent(new CustomEvent("configLoaded")); + }) + } + + #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(); + } + }); + }); + + // TODO: move from here in dedicated class + document.addEventListener("closeDialog", (ev: CustomEventInit) => { + ev.detail._element.closest(".ol-dialog").classList.add("hide"); + document.getElementById("gray-out")?.classList.toggle("hide", true); + }); + + /* Try and connect with the Olympus REST server */ + const loginForm = document.getElementById("authentication-form"); + if (loginForm instanceof HTMLFormElement) { + loginForm.addEventListener("submit", (ev:SubmitEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + var hash = sha256.create(); + const username = (loginForm.querySelector("#username") as HTMLInputElement).value; + const password = hash.update((loginForm.querySelector("#password") as HTMLInputElement).value).hex(); + + // Update the user credentials + this.getServerManager().setCredentials(username, password); + + // Start periodically requesting updates + this.getServerManager().startUpdate(); + + this.setLoginStatus("connecting"); + }); + } else { + console.error("Unable to find login form."); + } + + /* Temporary */ + this.getServerManager().setCredentials("admin", "4b8823ed9e5c2392ab4a791913bb8ce41956ea32e308b760eefb97536746dd33"); + this.getServerManager().startUpdate(); + + /* Reload the page, used to mimic a restart of the app */ + document.addEventListener("reloadPage", () => { + location.reload(); + }) + + ///* Inject the svgs with the corresponding svg code. This allows to dynamically manipulate the svg, like changing colors */ + //document.querySelectorAll("[inject-svg]").forEach((el: Element) => { + // var img = el as HTMLImageElement; + // var isLoaded = img.complete; + // if (isLoaded) + // SVGInjector(img); + // else + // img.addEventListener("load", () => { SVGInjector(img); }); + //}) + } + + 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 new file mode 100644 index 00000000..9173b2b1 --- /dev/null +++ b/frontend/react/src/other/eventsmanager.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..c889e713 --- /dev/null +++ b/frontend/react/src/other/manager.ts @@ -0,0 +1,37 @@ +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/other/utils.ts b/frontend/react/src/other/utils.ts new file mode 100644 index 00000000..ba660c1c --- /dev/null +++ b/frontend/react/src/other/utils.ts @@ -0,0 +1,476 @@ +import { LatLng, Point, Polygon } from "leaflet"; +import * as turf from "@turf/turf"; +import { UnitDatabase } from "../unit/databases/unitdatabase"; +import { aircraftDatabase } from "../unit/databases/aircraftdatabase"; +import { helicopterDatabase } from "../unit/databases/helicopterdatabase"; +import { groundUnitDatabase } from "../unit/databases/groundunitdatabase"; +//import { Buffer } from "buffer"; +import { GROUND_UNIT_AIR_DEFENCE_REGEX, ROEs, emissionsCountermeasures, reactionsToThreat, states } from "../constants/constants"; +import { navyUnitDatabase } from "../unit/databases/navyunitdatabase"; +import { DateAndTime, UnitBlueprint } from "../interfaces"; +import { Converter } from "usng"; + + +export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) { + const φ1 = deg2rad(lat1); // φ, λ in radians + const φ2 = deg2rad(lat2); + const λ1 = deg2rad(lon1); // φ, λ in radians + const λ2 = deg2rad(lon2); + const y = Math.sin(λ2 - λ1) * Math.cos(φ2); + const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1); + const θ = Math.atan2(y, x); + const brng = (rad2deg(θ) + 360) % 360; // in degrees + + return brng; +} + +export function distance(lat1: number, lon1: number, lat2: number, lon2: number) { + const R = 6371e3; // metres + const φ1 = deg2rad(lat1); // φ, λ in radians + const φ2 = deg2rad(lat2); + const Δφ = deg2rad(lat2 - lat1); + const Δλ = deg2rad(lon2 - lon1); + + const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + const d = R * c; // in metres + + return d; +} + +export function bearingAndDistanceToLatLng(lat: number, lon: number, brng: number, dist: number) { + const R = 6371e3; // metres + const φ1 = deg2rad(lat); // φ, λ in radians + const λ1 = deg2rad(lon); + const φ2 = Math.asin(Math.sin(φ1) * Math.cos(dist / R) + Math.cos(φ1) * Math.sin(dist / R) * Math.cos(brng)); + const λ2 = λ1 + Math.atan2(Math.sin(brng) * Math.sin(dist / R) * Math.cos(φ1), Math.cos(dist / R) - Math.sin(φ1) * Math.sin(φ2)); + + return new LatLng(rad2deg(φ2), rad2deg(λ2)); +} + +export function ConvertDDToDMS(D: number, lng: boolean) { + var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N"; + var deg = 0 | (D < 0 ? (D = -D) : D); + var min = 0 | (((D += 1e-9) % 1) * 60); + var sec = (0 | (((D * 60) % 1) * 6000)) / 100; + var dec = Math.round((sec - Math.floor(sec)) * 100); + var sec = Math.floor(sec); + if (lng) + return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\""; + else + return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\""; +} + +export function dataPointMap(container: HTMLElement, data: any) { + Object.keys(data).forEach((key) => { + const val = "" + data[key]; // Ensure a string + container.querySelectorAll(`[data-point="${key}"]`).forEach(el => { + // We could probably have options here + if (el instanceof HTMLInputElement) { + el.value = val; + } else if (el instanceof HTMLElement) { + el.innerText = val; + } + }); + }); +} + +export function deg2rad(deg: number) { + var pi = Math.PI; + return deg * (pi / 180); +} + +export function rad2deg(rad: number) { + var pi = Math.PI; + return rad / (pi / 180); +} + +export function generateUUIDv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +export function keyEventWasInInput(event: KeyboardEvent) { + const target = event.target; + return (target instanceof HTMLElement && (["INPUT", "TEXTAREA"].includes(target.nodeName))); +} + +export function reciprocalHeading(heading: number): number { + return heading > 180 ? heading - 180 : heading + 180; +} + +/** + * Prepend numbers to the start of a string + * + * @param num subject number + * @param places places to pad + * @param decimal whether this is a decimal number or not + * + * */ +export const zeroAppend = function (num: number, places: number, decimal: boolean = false) { + var string = decimal ? num.toFixed(2) : String(num); + while (string.length < places) { + string = "0" + string; + } + return string; +} + +export const zeroPad = function (num: number, places: number) { + var string = String(num); + while (string.length < places) { + string += "0"; + } + return string; +} + +export function similarity(s1: string, s2: string) { + var longer = s1; + var shorter = s2; + if (s1.length < s2.length) { + longer = s2; + shorter = s1; + } + var longerLength = longer.length; + if (longerLength == 0) { + return 1.0; + } + return (longerLength - editDistance(longer, shorter)) / longerLength; +} + +export function editDistance(s1: string, s2: string) { + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + var costs = new Array(); + for (var i = 0; i <= s1.length; i++) { + var lastValue = i; + for (var j = 0; j <= s2.length; j++) { + if (i == 0) + costs[j] = j; + else { + if (j > 0) { + var newValue = costs[j - 1]; + if (s1.charAt(i - 1) != s2.charAt(j - 1)) + newValue = Math.min(Math.min(newValue, lastValue), + costs[j]) + 1; + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + } + if (i > 0) + costs[s2.length] = lastValue; + } + return costs[s2.length]; +} + +export type MGRS = { + bandLetter: string, + columnLetter: string, + easting: string, + groups: string[], + northing: string, + precision: number, + rowLetter: string, + string: string, + zoneNumber: string +} + +export function latLngToMGRS(lat: number, lng: number, precision: number = 4): MGRS | false { + + if (precision < 0 || precision > 6) { + console.error("latLngToMGRS: precision must be a number >= 0 and <= 6. Given precision: " + precision); + return false; + } + const mgrs = new Converter({}).LLtoMGRS(lat, lng, precision); + const match = mgrs.match(new RegExp(`^(\\d{2})([A-Z])([A-Z])([A-Z])(\\d+)$`)); + const easting = match[5].substr(0, match[5].length / 2); + const northing = match[5].substr(match[5].length / 2); + + let output: MGRS = { + bandLetter: match[2], + columnLetter: match[3], + groups: [match[1] + match[2], match[3] + match[4], easting, northing], + easting: easting, + northing: northing, + precision: precision, + rowLetter: match[4], + string: match[0], + zoneNumber: match[1] + } + + return output; +} + +export function latLngToUTM(lat: number, lng: number) { + return new Converter({}).LLtoUTM(lat, lng); +} + +export function latLngToMercator(lat: number, lng: number): { x: number, y: number } { + var rMajor = 6378137; //Equatorial Radius, WGS84 + var shift = Math.PI * rMajor; + var x = lng * shift / 180; + var y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); + y = y * shift / 180; + + return { x: x, y: y }; +} + +export function mercatorToLatLng(x: number, y: number) { + var rMajor = 6378137; //Equatorial Radius, WGS84 + var shift = Math.PI * rMajor; + var lng = x / shift * 180.0; + var lat = y / shift * 180.0; + lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0); + + return { lng: lng, lat: lat }; +} + +export function createDivWithClass(className: string) { + var el = document.createElement("div"); + el.classList.add(className); + return el; +} + +export function knotsToMs(knots: number) { + return knots / 1.94384; +} + +export function msToKnots(ms: number) { + return ms * 1.94384; +} + +export function ftToM(ft: number) { + return ft * 0.3048; +} + +export function mToFt(m: number) { + return m / 0.3048; +} + +export function mToNm(m: number) { + return m * 0.000539957; +} + +export function nmToM(nm: number) { + return nm / 0.000539957; +} + +export function nmToFt(nm: number) { + return nm * 6076.12; +} + +export function polyContains(latlng: LatLng, polygon: Polygon) { + var poly = polygon.toGeoJSON(); + return turf.inside(turf.point([latlng.lng, latlng.lat]), poly); +} + +export function randomPointInPoly(polygon: Polygon): LatLng { + var bounds = polygon.getBounds(); + var x_min = bounds.getEast(); + var x_max = bounds.getWest(); + var y_min = bounds.getSouth(); + var y_max = bounds.getNorth(); + + var lat = y_min + (Math.random() * (y_max - y_min)); + var lng = x_min + (Math.random() * (x_max - x_min)); + + var poly = polygon.toGeoJSON(); + var inside = turf.inside(turf.point([lng, lat]), poly); + + if (inside) { + return new LatLng(lat, lng); + } else { + return randomPointInPoly(polygon); + } +} + +export function polygonArea(polygon: Polygon) { + var poly = polygon.toGeoJSON(); + return turf.area(poly); +} + +export function randomUnitBlueprint(unitDatabase: UnitDatabase, options: { type?: string, role?: string, ranges?: string[], eras?: string[], coalition?: string }) { + /* Start from all the unit blueprints in the database */ + var unitBlueprints = Object.values(unitDatabase.getBlueprints()); + + /* If a specific type or role is provided, use only the blueprints of that type or role */ + if (options.type && options.role) { + console.error("Can't create random unit if both type and role are provided. Either create by type or by role.") + return null; + } + + if (options.type) { + unitBlueprints = unitDatabase.getByType(options.type); + } + else if (options.role) { + unitBlueprints = unitDatabase.getByType(options.role); + } + + /* Keep only the units that have a range included in the requested values */ + if (options.ranges) { + unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { + var rangeType = ""; + var range = unitBlueprint.acquisitionRange; + if (range !== undefined) { + if (range >= 0 && range < 10000) + rangeType = "Short range"; + else if (range >= 10000 && range < 100000) + rangeType = "Medium range"; + else if (range >= 100000 && range < 999999) + rangeType = "Long range"; + } + return options.ranges?.includes(rangeType); + }); + } + + /* Keep only the units that have an era included in the requested values */ + if (options.eras) { + unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { + return unitBlueprint.era ? options.eras?.includes(unitBlueprint.era) : true; + }); + } + + /* Keep only the units that have the correct coalition, if selected */ + if (options.coalition) { + unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { + return (unitBlueprint.coalition && unitBlueprint.coalition !== "") ? options.coalition === unitBlueprint.coalition : true; + }); + } + + var index = Math.floor(Math.random() * unitBlueprints.length); + return unitBlueprints[index]; +} + +export function getMarkerCategoryByName(name: string) { + if (aircraftDatabase.getByName(name) != null) + return "aircraft"; + else if (helicopterDatabase.getByName(name) != null) + return "helicopter"; + else if (groundUnitDatabase.getByName(name) != null) { + var type = groundUnitDatabase.getByName(name)?.type ?? ""; + if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type)) + return "groundunit-sam"; + else + return "groundunit-other"; + } + else if (navyUnitDatabase.getByName(name) != null) + return "navyunit"; + else + return "aircraft"; // TODO add other unit types +} + +export function getUnitDatabaseByCategory(category: string) { + if (category.toLowerCase() == "aircraft") + return aircraftDatabase; + else if (category.toLowerCase() == "helicopter") + return helicopterDatabase; + else if (category.toLowerCase().includes("groundunit")) + return groundUnitDatabase; + else if (category.toLowerCase().includes("navyunit")) + return navyUnitDatabase; + else + return null; +} + +export function getCategoryBlueprintIconSVG(category: string, unitName: string) { + + const path = "/resources/theme/images/buttons/visibility/"; + + // We can just send these back okay + if (["Aircraft", "Helicopter", "NavyUnit"].includes(category)) return `${path}${category.toLowerCase()}.svg`; + + // Return if not a ground units as it's therefore something we don't recognise + if (category !== "GroundUnit") return false; + + /** We need to get the unit detail for ground units so we can work out if it's an air defence unit or not **/ + return GROUND_UNIT_AIR_DEFENCE_REGEX.test(unitName) ? `${path}groundunit-sam.svg` : `${path}groundunit.svg`; +} + +export function base64ToBytes(base64: string) { + //return Buffer.from(base64, 'base64').buffer; +} + +export function enumToState(state: number) { + if (state < states.length) + return states[state]; + else + return states[0]; +} + +export function enumToROE(ROE: number) { + if (ROE < ROEs.length) + return ROEs[ROE]; + else + return ROEs[0]; +} + +export function enumToReactionToThreat(reactionToThreat: number) { + if (reactionToThreat < reactionsToThreat.length) + return reactionsToThreat[reactionToThreat]; + else + return reactionsToThreat[0]; +} + +export function enumToEmissioNCountermeasure(emissionCountermeasure: number) { + if (emissionCountermeasure < emissionsCountermeasures.length) + return emissionsCountermeasures[emissionCountermeasure]; + else + return emissionsCountermeasures[0]; +} + +export function enumToCoalition(coalitionID: number) { + switch (coalitionID) { + case 0: return "neutral"; + case 1: return "red"; + case 2: return "blue"; + } + return ""; +} + +export function coalitionToEnum(coalition: string) { + switch (coalition) { + case "neutral": return 0; + case "red": return 1; + case "blue": return 2; + } + return 0; +} + + +export function convertDateAndTimeToDate(dateAndTime: DateAndTime) { + const date = dateAndTime.date; + const time = dateAndTime.time; + + if (!date) { + return new Date(); + } + + let year = date.Year; + let month = date.Month - 1; + + if (month < 0) { + month = 11; + year--; + } + + return new Date(year, month, date.Day, time.h, time.m, time.s); +} + +export function getGroundElevation(latlng: LatLng, callback: CallableFunction) { + /* Get the ground elevation from the server endpoint */ + const xhr = new XMLHttpRequest(); + xhr.open('GET', `api/elevation/${latlng.lat}/${latlng.lng}`, true); + xhr.timeout = 500; // ms + xhr.responseType = 'json'; + xhr.onload = () => { + var status = xhr?.status; + if (status === 200) { + callback(xhr.response) + } + }; + xhr.send(); +} \ No newline at end of file diff --git a/frontend/react/src/server/dataextractor.ts b/frontend/react/src/server/dataextractor.ts new file mode 100644 index 00000000..2f255ce1 --- /dev/null +++ b/frontend/react/src/server/dataextractor.ts @@ -0,0 +1,164 @@ +import { LatLng } from "leaflet"; +import { Ammo, Contact, GeneralSettings, Offset, Radio, TACAN } from "../interfaces"; + +export class DataExtractor { + #seekPosition = 0; + #dataview: DataView; + #decoder: TextDecoder; + #buffer: ArrayBuffer; + + constructor(buffer: ArrayBuffer) { + this.#buffer = buffer; + this.#dataview = new DataView(this.#buffer); + this.#decoder = new TextDecoder("utf-8"); + } + + setSeekPosition(seekPosition: number) { + this.#seekPosition = seekPosition; + } + + getSeekPosition() { + return this.#seekPosition; + } + + extractBool() { + const value = this.#dataview.getUint8(this.#seekPosition); + this.#seekPosition += 1; + return value > 0; + } + + extractUInt8() { + const value = this.#dataview.getUint8(this.#seekPosition); + this.#seekPosition += 1; + return value; + } + + extractUInt16() { + const value = this.#dataview.getUint16(this.#seekPosition, true); + this.#seekPosition += 2; + return value; + } + + extractUInt32() { + const value = this.#dataview.getUint32(this.#seekPosition, true); + this.#seekPosition += 4; + return value; + } + + extractUInt64() { + const value = this.#dataview.getBigUint64(this.#seekPosition, true); + this.#seekPosition += 8; + return value; + } + + extractFloat64() { + const value = this.#dataview.getFloat64(this.#seekPosition, true); + this.#seekPosition += 8; + return value; + } + + extractLatLng() { + return new LatLng(this.extractFloat64(), this.extractFloat64(), this.extractFloat64()) + } + + extractFromBitmask(bitmask: number, position: number) { + return ((bitmask >> position) & 1) > 0; + } + + extractString(length?: number) { + if (length === undefined) + length = this.extractUInt16() + var stringBuffer = this.#buffer.slice(this.#seekPosition, this.#seekPosition + length); + var view = new Int8Array(stringBuffer); + var stringLength = length; + view.every((value: number, idx: number) => { + if (value === 0) { + stringLength = idx; + return false; + } else + return true; + }); + const value = this.#decoder.decode(stringBuffer); + this.#seekPosition += length; + return value.substring(0, stringLength).trim(); + } + + extractChar() { + return this.extractString(1); + } + + extractTACAN() { + const value: TACAN = { + isOn: this.extractBool(), + channel: this.extractUInt8(), + XY: this.extractChar(), + callsign: this.extractString(4) + } + return value; + } + + extractRadio() { + const value: Radio = { + frequency: this.extractUInt32(), + callsign: this.extractUInt8(), + callsignNumber: this.extractUInt8() + } + return value; + } + + extractGeneralSettings() { + const value: GeneralSettings = { + prohibitJettison: this.extractBool(), + prohibitAA: this.extractBool(), + prohibitAG: this.extractBool(), + prohibitAfterburner: this.extractBool(), + prohibitAirWpn: this.extractBool(), + } + return value; + } + + extractAmmo() { + const value: Ammo[] = []; + const size = this.extractUInt16(); + for (let idx = 0; idx < size; idx++) { + value.push({ + quantity: this.extractUInt16(), + name: this.extractString(33), + guidance: this.extractUInt8(), + category: this.extractUInt8(), + missileCategory: this.extractUInt8() + }); + } + return value; + } + + extractContacts(){ + const value: Contact[] = []; + const size = this.extractUInt16(); + for (let idx = 0; idx < size; idx++) { + value.push({ + ID: this.extractUInt32(), + detectionMethod: this.extractUInt8() + }); + } + return value; + } + + extractActivePath() { + const value: LatLng[] = []; + const size = this.extractUInt16(); + for (let idx = 0; idx < size; idx++) { + value.push(this.extractLatLng()); + } + return value; + } + + extractOffset() { + const value: Offset = { + x: this.extractFloat64(), + y: this.extractFloat64(), + z: this.extractFloat64(), + } + return value; + } +} \ No newline at end of file diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts new file mode 100644 index 00000000..7073b544 --- /dev/null +++ b/frontend/react/src/server/servermanager.ts @@ -0,0 +1,604 @@ +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, TACAN } from '../interfaces'; +import { zeroAppend } from '../other/utils'; + +export class ServerManager { + #connected: boolean = false; + #paused: boolean = false; + #REST_ADDRESS = "http://localhost:3001/olympus"; + #username = ""; + #password = ""; + #sessionHash: string | null = null; + #lastUpdateTimes: { [key: string]: number } = {} + #previousMissionElapsedTime: number = 0; // Track if mission elapsed time is increasing (i.e. is the server paused) + #serverIsPaused: boolean = false; + #intervals: number[] = []; + #requests: { [key: string]: XMLHttpRequest } = {}; + + constructor() { + this.#lastUpdateTimes[UNITS_URI] = Date.now(); + this.#lastUpdateTimes[WEAPONS_URI] = Date.now(); + this.#lastUpdateTimes[LOGS_URI] = Date.now(); + this.#lastUpdateTimes[AIRBASES_URI] = Date.now(); + this.#lastUpdateTimes[BULLSEYE_URI] = Date.now(); + this.#lastUpdateTimes[MISSION_URI] = Date.now(); + } + + setCredentials(newUsername: string, newPassword: string) { + this.#username = newUsername; + this.#password = newPassword; + } + + GET(callback: CallableFunction, uri: string, options?: ServerRequestOptions, responseType: string = 'text', force: boolean = false) { + var xmlHttp = new XMLHttpRequest(); + + /* If a request on this uri is still pending (meaning it's not done or did not yet fail), skip the request, to avoid clogging the TCP workers */ + /* If we are forcing the request we don't care if one already exists, just send it. CAREFUL: this makes sense only for low frequency requests, like refreshes, when we + are reasonably confident any previous request will be done before we make a new one on the same URI. */ + if (uri in this.#requests && this.#requests[uri].readyState !== 4 && !force) { + console.warn(`GET request on ${uri} URI still pending, skipping...`); + return; + } + + if (!force) + this.#requests[uri] = xmlHttp; + + /* Assemble the request options string */ + var optionsString = ''; + if (options?.time != undefined) + optionsString = `time=${options.time}`; + if (options?.commandHash != undefined) + optionsString = `commandHash=${options.commandHash}`; + + /* On the connection */ + xmlHttp.open("GET", `${this.#REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ''}`, true); + + /* If provided, set the credentials */ + if (this.#username && this.#password) + xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`)); + + /* If specified, set the response type */ + if (responseType) + xmlHttp.responseType = responseType as XMLHttpRequestResponseType; + + xmlHttp.onload = (e) => { + if (xmlHttp.status == 200) { + /* Success */ + this.setConnected(true); + if (xmlHttp.responseType == 'arraybuffer') + this.#lastUpdateTimes[uri] = callback(xmlHttp.response); + else { + const result = JSON.parse(xmlHttp.responseText); + this.#lastUpdateTimes[uri] = callback(result); + + //if (result.frameRate !== undefined && result.load !== undefined) + // (getApp().getPanelsManager().get("serverStatus") as ServerStatusPanel).update(result.frameRate, result.load); + } + } else if (xmlHttp.status == 401) { + /* Bad credentials */ + console.error("Incorrect username/password"); + getApp().setLoginStatus("failed"); + } else { + /* Failure, probably disconnected */ + this.setConnected(false); + } + }; + xmlHttp.onreadystatechange = (res) => { + if (xmlHttp.readyState == 4 && xmlHttp.status === 0) { + console.error("An error occurred during the XMLHttpRequest"); + this.setConnected(false); + } + }; + xmlHttp.send(null); + } + + PUT(request: object, callback: CallableFunction) { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("PUT", this.#REST_ADDRESS); + xmlHttp.setRequestHeader("Content-Type", "application/json"); + if (this.#username && this.#password) + xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`)); + xmlHttp.onload = (res: any) => { + var res = JSON.parse(xmlHttp.responseText); + callback(res); + }; + xmlHttp.send(JSON.stringify(request)); + } + + getConfig(callback: CallableFunction) { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("GET", window.location.href.split('?')[0] + "config", true); + xmlHttp.onload = function (e) { + var data = JSON.parse(xmlHttp.responseText); + callback(data); + }; + xmlHttp.onerror = function () { + console.error("An error occurred during the XMLHttpRequest, could not retrieve configuration file"); + }; + xmlHttp.send(null); + } + + setAddress(address: string) { + // Temporary + address = "http://localhost:3000/" + this.#REST_ADDRESS = `${address}olympus` + console.log(`Setting REST address to ${this.#REST_ADDRESS}`) + } + + getAirbases(callback: CallableFunction) { + this.GET(callback, AIRBASES_URI); + } + + getBullseye(callback: CallableFunction) { + this.GET(callback, BULLSEYE_URI); + } + + getLogs(callback: CallableFunction, refresh: boolean = false) { + this.GET(callback, LOGS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[LOGS_URI] }, 'text', refresh); + } + + getMission(callback: CallableFunction) { + this.GET(callback, MISSION_URI); + } + + getUnits(callback: CallableFunction, refresh: boolean = false) { + this.GET(callback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, 'arraybuffer', refresh); + } + + getWeapons(callback: CallableFunction, refresh: boolean = false) { + this.GET(callback, WEAPONS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[WEAPONS_URI] }, 'arraybuffer', refresh); + } + + isCommandExecuted(callback: CallableFunction, commandHash: string) { + this.GET(callback, COMMANDS_URI, { commandHash: commandHash }); + } + + addDestination(ID: number, path: any, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "path": path } + var data = { "setPath": command } + this.PUT(data, callback); + } + + spawnSmoke(color: string, latlng: LatLng, callback: CallableFunction = () => { }) { + var command = { "color": color, "location": latlng }; + var data = { "smoke": command } + this.PUT(data, callback); + } + + spawnExplosion(intensity: number, explosionType: string, latlng: LatLng, callback: CallableFunction = () => { }) { + var command = { "explosionType": explosionType, "intensity": intensity, "location": latlng }; + var data = { "explosion": command } + this.PUT(data, callback); + } + + spawnAircrafts(units: any, coalition: string, airbaseName: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => { }) { + var command = { "units": units, "coalition": coalition, "airbaseName": airbaseName, "country": country, "immediate": immediate, "spawnPoints": spawnPoints }; + var data = { "spawnAircrafts": command } + this.PUT(data, callback); + } + + spawnHelicopters(units: any, coalition: string, airbaseName: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => { }) { + var command = { "units": units, "coalition": coalition, "airbaseName": airbaseName, "country": country, "immediate": immediate, "spawnPoints": spawnPoints }; + var data = { "spawnHelicopters": command } + this.PUT(data, callback); + } + + spawnGroundUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => { }) { + var command = { "units": units, "coalition": coalition, "country": country, "immediate": immediate, "spawnPoints": spawnPoints };; + var data = { "spawnGroundUnits": command } + this.PUT(data, callback); + } + + spawnNavyUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => { }) { + var command = { "units": units, "coalition": coalition, "country": country, "immediate": immediate, "spawnPoints": spawnPoints }; + var data = { "spawnNavyUnits": command } + this.PUT(data, callback); + } + + attackUnit(ID: number, targetID: number, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "targetID": targetID }; + var data = { "attackUnit": command } + this.PUT(data, callback); + } + + followUnit(ID: number, targetID: number, offset: { "x": number, "y": number, "z": number }, callback: CallableFunction = () => { }) { + // X: front-rear, positive front + // Y: top-bottom, positive bottom + // Z: left-right, positive right + + var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"] }; + var data = { "followUnit": command } + this.PUT(data, callback); + } + + cloneUnits(units: { ID: number, location: LatLng }[], deleteOriginal: boolean, spawnPoints: number, callback: CallableFunction = () => { }) { + var command = { "units": units, "deleteOriginal": deleteOriginal, "spawnPoints": spawnPoints }; + var data = { "cloneUnits": command } + this.PUT(data, callback); + } + + deleteUnit(ID: number, explosion: boolean, explosionType: string, immediate: boolean, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "explosion": explosion, "explosionType": explosionType, "immediate": immediate }; + var data = { "deleteUnit": command } + this.PUT(data, callback); + } + + landAt(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "location": latlng }; + var data = { "landAt": command } + this.PUT(data, callback); + } + + changeSpeed(ID: number, speedChange: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "change": speedChange } + var data = { "changeSpeed": command } + this.PUT(data, callback); + } + + setSpeed(ID: number, speed: number, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "speed": speed } + var data = { "setSpeed": command } + this.PUT(data, callback); + } + + setSpeedType(ID: number, speedType: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "speedType": speedType } + var data = { "setSpeedType": command } + this.PUT(data, callback); + } + + changeAltitude(ID: number, altitudeChange: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "change": altitudeChange } + var data = { "changeAltitude": command } + this.PUT(data, callback); + } + + setAltitudeType(ID: number, altitudeType: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "altitudeType": altitudeType } + var data = { "setAltitudeType": command } + this.PUT(data, callback); + } + + setAltitude(ID: number, altitude: number, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "altitude": altitude } + var data = { "setAltitude": command } + this.PUT(data, callback); + } + + createFormation(ID: number, isLeader: boolean, wingmenIDs: number[], callback: CallableFunction = () => { }) { + var command = { "ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader } + var data = { "setLeader": command } + this.PUT(data, callback); + } + + setROE(ID: number, ROE: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "ROE": ROEs.indexOf(ROE) } + var data = { "setROE": command } + this.PUT(data, callback); + } + + setReactionToThreat(ID: number, reactionToThreat: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "reactionToThreat": reactionsToThreat.indexOf(reactionToThreat) } + var data = { "setReactionToThreat": command } + this.PUT(data, callback); + } + + setEmissionsCountermeasures(ID: number, emissionCountermeasure: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "emissionsCountermeasures": emissionsCountermeasures.indexOf(emissionCountermeasure) } + var data = { "setEmissionsCountermeasures": command } + this.PUT(data, callback); + } + + setOnOff(ID: number, onOff: boolean, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "onOff": onOff } + var data = { "setOnOff": command } + this.PUT(data, callback); + } + + setFollowRoads(ID: number, followRoads: boolean, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "followRoads": followRoads } + var data = { "setFollowRoads": command } + this.PUT(data, callback); + } + + setOperateAs(ID: number, operateAs: number, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "operateAs": operateAs } + var data = { "setOperateAs": command } + this.PUT(data, callback); + } + + + refuel(ID: number, callback: CallableFunction = () => { }) { + var command = { "ID": ID }; + var data = { "refuel": command } + this.PUT(data, callback); + } + + bombPoint(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "location": latlng } + var data = { "bombPoint": command } + this.PUT(data, callback); + } + + carpetBomb(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "location": latlng } + var data = { "carpetBomb": command } + this.PUT(data, callback); + } + + bombBuilding(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "location": latlng } + var data = { "bombBuilding": command } + this.PUT(data, callback); + } + + fireAtArea(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "location": latlng } + var data = { "fireAtArea": command } + this.PUT(data, callback); + } + + simulateFireFight(ID: number, latlng: LatLng, altitude: number, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "location": latlng, "altitude": altitude } + var data = { "simulateFireFight": command } + this.PUT(data, callback); + } + + // TODO: Remove coalition + scenicAAA(ID: number, coalition: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "coalition": coalition } + var data = { "scenicAAA": command } + this.PUT(data, callback); + } + + // TODO: Remove coalition + missOnPurpose(ID: number, coalition: string, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "coalition": coalition } + var data = { "missOnPurpose": command } + this.PUT(data, callback); + } + + landAtPoint(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "location": latlng } + var data = { "landAtPoint": command } + this.PUT(data, callback); + } + + setShotsScatter(ID: number, shotsScatter: number, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "shotsScatter": shotsScatter } + var data = { "setShotsScatter": command } + this.PUT(data, callback); + } + + setShotsIntensity(ID: number, shotsIntensity: number, callback: CallableFunction = () => { }) { + var command = { "ID": ID, "shotsIntensity": shotsIntensity } + var data = { "setShotsIntensity": command } + this.PUT(data, callback); + } + + setAdvacedOptions(ID: number, isActiveTanker: boolean, isActiveAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, callback: CallableFunction = () => { }) { + var command = { + "ID": ID, + "isActiveTanker": isActiveTanker, + "isActiveAWACS": isActiveAWACS, + "TACAN": TACAN, + "radio": radio, + "generalSettings": generalSettings + }; + + var data = { "setAdvancedOptions": command }; + this.PUT(data, callback); + } + + setCommandModeOptions(restrictSpawns: boolean, restrictToCoalition: boolean, spawnPoints: { blue: number, red: number }, eras: string[], setupTime: number, callback: CallableFunction = () => { }) { + var command = { + "restrictSpawns": restrictSpawns, + "restrictToCoalition": restrictToCoalition, + "spawnPoints": spawnPoints, + "eras": eras, + "setupTime": setupTime + }; + + var data = { "setCommandModeOptions": command }; + this.PUT(data, callback); + } + + reloadDatabases(callback: CallableFunction = () => { }) { + var data = { "reloadDatabases": {} }; + this.PUT(data, callback); + } + + startUpdate() { + /* Clear any existing interval */ + this.#intervals.forEach((interval: number) => { window.clearInterval(interval); }); + this.#intervals = []; + + this.#intervals.push(window.setInterval(() => { + if (!this.getPaused()) { + this.getMission((data: MissionData) => { + this.checkSessionHash(data.sessionHash); + getApp().getMissionManager()?.updateMission(data); + return data.time; + }); + } + }, 1000)); + + this.#intervals.push(window.setInterval(() => { + if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) { + this.getAirbases((data: AirbasesData) => { + this.checkSessionHash(data.sessionHash); + getApp().getMissionManager()?.updateAirbases(data); + return data.time; + }); + } + }, 10000)); + + this.#intervals.push(window.setInterval(() => { + if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) { + this.getBullseye((data: BullseyesData) => { + this.checkSessionHash(data.sessionHash); + getApp().getMissionManager()?.updateBullseyes(data); + return data.time; + }); + } + }, 10000)); + + this.#intervals.push(window.setInterval(() => { + if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) { + this.getLogs((data: any) => { + this.checkSessionHash(data.sessionHash); + (getApp().getPanelsManager().get("log") as LogPanel).appendLogs(data.logs) + return data.time; + }); + } + }, 1000)); + + this.#intervals.push(window.setInterval(() => { + if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) { + this.getUnits((buffer: ArrayBuffer) => { + var time = getApp().getUnitsManager()?.update(buffer); + return time; + }, false); + } + }, 250)); + + this.#intervals.push(window.setInterval(() => { + if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) { + this.getWeapons((buffer: ArrayBuffer) => { + var time = getApp().getWeaponsManager()?.update(buffer); + return time; + }, false); + } + }, 250)); + + this.#intervals.push(window.setInterval(() => { + if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) { + this.getUnits((buffer: ArrayBuffer) => { + var time = getApp().getUnitsManager()?.update(buffer); + return time; + }, true); + + const elapsedMissionTime = getApp().getMissionManager().getDateAndTime().elapsedTime; + this.#serverIsPaused = (elapsedMissionTime === this.#previousMissionElapsedTime); + this.#previousMissionElapsedTime = elapsedMissionTime; + + const csp = (getApp().getPanelsManager().get("connectionStatus") as ConnectionStatusPanel); + + if (this.getConnected()) { + if (this.getServerIsPaused()) { + csp.showServerPaused(); + } else { + csp.showConnected(); + } + } else { + csp.showDisconnected(); + } + + } + }, (this.getServerIsPaused() ? 500 : 5000))); + + // Mission clock and elapsed time + this.#intervals.push(window.setInterval(() => { + + if (!this.getConnected() || this.#serverIsPaused) { + return; + } + + const elapsedMissionTime = getApp().getMissionManager().getDateAndTime().elapsedTime; + + const csp = (getApp().getPanelsManager().get("connectionStatus") as ConnectionStatusPanel); + const mt = getApp().getMissionManager().getDateAndTime().time; + + csp.setMissionTime([mt.h, mt.m, mt.s].map(n => zeroAppend(n, 2)).join(":")); + csp.setElapsedTime(new Date(elapsedMissionTime * 1000).toISOString().substring(11, 19)); + + }, 1000)); + + this.#intervals.push(window.setInterval(() => { + if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) { + this.getWeapons((buffer: ArrayBuffer) => { + var time = getApp().getWeaponsManager()?.update(buffer); + return time; + }, true); + } + }, 5000)); + } + + refreshAll() { + this.getAirbases((data: AirbasesData) => { + this.checkSessionHash(data.sessionHash); + getApp().getMissionManager()?.updateAirbases(data); + return data.time; + }); + + this.getBullseye((data: BullseyesData) => { + this.checkSessionHash(data.sessionHash); + getApp().getMissionManager()?.updateBullseyes(data); + return data.time; + }); + + this.getLogs((data: any) => { + this.checkSessionHash(data.sessionHash); + (getApp().getPanelsManager().get("log") as LogPanel).appendLogs(data.logs) + return data.time; + }); + + this.getWeapons((buffer: ArrayBuffer) => { + var time = getApp().getWeaponsManager()?.update(buffer); + return time; + }, true); + + this.getUnits((buffer: ArrayBuffer) => { + var time = getApp().getUnitsManager()?.update(buffer); + return time; + }, true); + } + + checkSessionHash(newSessionHash: string) { + if (this.#sessionHash != null) { + if (newSessionHash !== this.#sessionHash) + location.reload(); + } + else + this.#sessionHash = newSessionHash; + } + + setConnected(newConnected: boolean) { + if (this.#connected != newConnected) { + //newConnected ? (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Connected to DCS Olympus server") : (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Disconnected from DCS Olympus server"); + if (newConnected) { + document.getElementById("splash-screen")?.classList.add("hide"); + document.getElementById("gray-out")?.classList.add("hide"); + } + } + + this.#connected = newConnected; + } + + getConnected() { + return this.#connected; + } + + setPaused(newPaused: boolean) { + this.#paused = newPaused; + this.#paused ? (getApp().getPopupsManager().get("infoPopup") as Popup).setText("View paused") : (getApp().getPopupsManager().get("infoPopup") as Popup).setText("View unpaused"); + } + + getPaused() { + return this.#paused; + } + + getServerIsPaused() { + return this.#serverIsPaused; + } + + getRequests() { + return this.#requests; + } +} diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts new file mode 100644 index 00000000..77228d79 --- /dev/null +++ b/frontend/react/src/shortcut/shortcut.ts @@ -0,0 +1,48 @@ +import { getApp } from "../olympusapp"; +import { ShortcutKeyboardOptions, ShortcutMouseOptions, ShortcutOptions } from "../interfaces"; +import { keyEventWasInInput } from "../other/utils"; + +export abstract class Shortcut { + #config: ShortcutOptions + + constructor(config: ShortcutOptions) { + this.#config = config; + } + + getConfig() { + return this.#config; + } +} + +export class ShortcutKeyboard extends Shortcut { + constructor(config: ShortcutKeyboardOptions) { + config.event = config.event || "keyup"; + super(config); + + document.addEventListener(config.event, (ev: any) => { + if ( typeof config.context === "string" && !getApp().getContextManager().currentContextIs( config.context ) ) { + return; + } + + if (ev instanceof KeyboardEvent === false || keyEventWasInInput(ev)) { + return; + } + + if (config.code !== ev.code) { + return; + } + + if (((typeof config.altKey !== "boolean") || (typeof config.altKey === "boolean" && ev.altKey === config.altKey)) + && ((typeof config.ctrlKey !== "boolean") || (typeof config.ctrlKey === "boolean" && ev.ctrlKey === config.ctrlKey)) + && ((typeof config.shiftKey !== "boolean") || (typeof config.shiftKey === "boolean" && ev.shiftKey === config.shiftKey))) { + config.callback(ev); + } + }); + } +} + +export class ShortcutMouse extends Shortcut { + constructor(config: ShortcutMouseOptions) { + super(config); + } +} \ No newline at end of file diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts new file mode 100644 index 00000000..8d86f741 --- /dev/null +++ b/frontend/react/src/shortcut/shortcutmanager.ts @@ -0,0 +1,65 @@ +import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces"; +import { Manager } from "../other/manager"; + +import { ShortcutKeyboard, ShortcutMouse } from "./shortcut"; + +export class ShortcutManager extends Manager { + + #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) + } + this.#keyDownCallbacks.forEach(callback => callback(ev)); + }); + + document.addEventListener("keyup", (ev: KeyboardEvent) => { + this.#keysBeingHeld = this.#keysBeingHeld.filter(held => held !== ev.code); + this.#keyUpCallbacks.forEach(callback => callback(ev)); + }); + + } + + add(name: string, shortcut: any) { + console.error("ShortcutManager:add() cannot be used. Use addKeyboardShortcut or addMouseShortcut."); + return this; + } + + addKeyboardShortcut(name: string, shortcutKeyboardOptions: ShortcutKeyboardOptions) { + super.add(name, new ShortcutKeyboard(shortcutKeyboardOptions)); + return this; + } + + addMouseShortcut(name: string, shortcutMouseOptions: ShortcutMouseOptions) { + super.add(name, new ShortcutMouse(shortcutMouseOptions)); + return this; + } + + getKeysBeingHeld() { + return this.#keysBeingHeld; + } + + keyComboMatches(combo: string[]) { + const heldKeys = this.getKeysBeingHeld(); + if (combo.length !== heldKeys.length) { + return false; + } + + return combo.every(key => heldKeys.indexOf(key) > -1); + } + + onKeyDown(callback: CallableFunction) { + this.#keyDownCallbacks.push(callback); + } + + onKeyUp(callback: CallableFunction) { + this.#keyUpCallbacks.push(callback); + } +} \ No newline at end of file diff --git a/frontend/react/src/statecontext.tsx b/frontend/react/src/statecontext.tsx index b42dfb8a..0dfc9de5 100644 --- a/frontend/react/src/statecontext.tsx +++ b/frontend/react/src/statecontext.tsx @@ -1,5 +1,5 @@ import { createContext } from "react"; -import { OlympusState } from "./App"; +import { OlympusState } from "./ui"; export const StateContext = createContext({ spawnMenuVisible: false, diff --git a/frontend/react/src/App.css b/frontend/react/src/ui.css similarity index 100% rename from frontend/react/src/App.css rename to frontend/react/src/ui.css diff --git a/frontend/react/src/App.tsx b/frontend/react/src/ui.tsx similarity index 87% rename from frontend/react/src/App.tsx rename to frontend/react/src/ui.tsx index 7d9fd16f..c7a7f4f4 100644 --- a/frontend/react/src/App.tsx +++ b/frontend/react/src/ui.tsx @@ -1,12 +1,13 @@ import React from 'react' -import './app.css' +import './ui.css' import { MapContainer, TileLayer } from 'react-leaflet' import { Map } from './map/map' -import { Header } from './ui/header' +import { Header } from './ui/panels/header' import { EventsProvider } from './eventscontext' import { StateProvider } from './statecontext' +import { SpawnMenu } from './ui/panels/spawnmenu' const position = [51.505, -0.09] @@ -17,7 +18,7 @@ export type OlympusState = { drawingMenuVisible: boolean } -export default class App extends React.Component<{}, OlympusState> { +export default class UI extends React.Component<{}, OlympusState> { constructor(props) { super(props); @@ -114,7 +115,7 @@ export default class App extends React.Component<{}, OlympusState> { render() { return ( -
+
{ toggleDrawingMenu: this.toggleDrawingMenu } }> - - - -
+
+ +
+
+
+ +
+
diff --git a/frontend/react/src/ui/statebuttons.tsx b/frontend/react/src/ui/buttons/statebutton.tsx similarity index 93% rename from frontend/react/src/ui/statebuttons.tsx rename to frontend/react/src/ui/buttons/statebutton.tsx index 965fe6f1..5a69867f 100644 --- a/frontend/react/src/ui/statebuttons.tsx +++ b/frontend/react/src/ui/buttons/statebutton.tsx @@ -18,7 +18,7 @@ export class StateButton extends React.Component { render() { var computedClassName = ""; - computedClassName += this.props.active? 'bg-white text-background-steel': 'bg-transparent text-white border-white'; + computedClassName += this.props.active? 'bg-white text-background-darker': 'bg-transparent text-white border-white'; return ( diff --git a/frontend/react/src/ui/panels/components/menutitle.tsx b/frontend/react/src/ui/panels/components/menutitle.tsx new file mode 100644 index 00000000..e7383d4f --- /dev/null +++ b/frontend/react/src/ui/panels/components/menutitle.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +export class MenuTitle extends React.Component<{title: string}, {}> { + render() { + return
+ {this.props.title} +
+ } +} diff --git a/frontend/react/src/ui/header.tsx b/frontend/react/src/ui/panels/header.tsx similarity index 80% rename from frontend/react/src/ui/header.tsx rename to frontend/react/src/ui/panels/header.tsx index 0a768bfe..7a669f95 100644 --- a/frontend/react/src/ui/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -1,9 +1,9 @@ import React from 'react' -import { StateButton } from './statebuttons'; +import { StateButton } from '../buttons/statebutton'; import { faPlus, faGamepad, faRuler, faPencil } from '@fortawesome/free-solid-svg-icons'; import { library } from '@fortawesome/fontawesome-svg-core' -import { EventsConsumer, EventsContext } from '../eventscontext'; -import { StateConsumer } from '../statecontext'; +import { EventsConsumer, EventsContext } from '../../eventscontext'; +import { StateConsumer } from '../../statecontext'; library.add(faPlus, faGamepad, faRuler, faPencil) @@ -18,7 +18,7 @@ export class Header extends React.Component<{}, {}> { {(appState) => {(events) => -
+
diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx new file mode 100644 index 00000000..21889c4b --- /dev/null +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { MenuTitle } from "./components/menutitle"; + +export class SpawnMenu extends React.Component<{}, {}> { + constructor(props) { + super(props); + } + + render() { + return
+ +
+ } +} \ No newline at end of file diff --git a/frontend/react/src/unit/contextaction.ts b/frontend/react/src/unit/contextaction.ts new file mode 100644 index 00000000..dd3a4b6b --- /dev/null +++ b/frontend/react/src/unit/contextaction.ts @@ -0,0 +1,60 @@ +import { Unit } from "./unit"; + +export interface ContextActionOptions { + isScenic?: boolean +} + +export class ContextAction { + #id: string = ""; + #label: string = ""; + #description: string = ""; + #callback: CallableFunction | null = null; + #units: Unit[] = []; + #hideContextAfterExecution: boolean = true + #options: ContextActionOptions; + + constructor(id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true, options: ContextActionOptions) { + this.#id = id; + this.#label = label; + this.#description = description; + this.#callback = callback; + this.#hideContextAfterExecution = hideContextAfterExecution; + this.#options = { + "isScenic": false, + ...options + } + } + + addUnit(unit: Unit) { + this.#units.push(unit); + } + + getId() { + return this.#id; + } + + getLabel() { + return this.#label; + } + + getOptions() { + return this.#options; + } + + getDescription() { + return this.#description; + } + + getCallback() { + return this.#callback; + } + + executeCallback() { + if (this.#callback) + this.#callback(this.#units); + } + + getHideContextAfterExecution() { + return this.#hideContextAfterExecution; + } +} diff --git a/frontend/react/src/unit/contextactionset.ts b/frontend/react/src/unit/contextactionset.ts new file mode 100644 index 00000000..d061cd1d --- /dev/null +++ b/frontend/react/src/unit/contextactionset.ts @@ -0,0 +1,25 @@ +import { ContextAction, ContextActionOptions } from "./contextaction"; +import { Unit } from "./unit"; + +export class ContextActionSet { + #contextActions: {[key: string]: ContextAction} = {}; + + constructor() { + + } + + addContextAction(unit: Unit, id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true, options?:ContextActionOptions) { + options = options || {}; + + if (!(id in this.#contextActions)) { + this.#contextActions[id] = new ContextAction(id, label, description, callback, hideContextAfterExecution, options); + } + this.#contextActions[id].addUnit(unit); + } + + getContextActions() { + return this.#contextActions; + } + + +} \ No newline at end of file diff --git a/frontend/react/src/unit/databases/aircraftdatabase.ts b/frontend/react/src/unit/databases/aircraftdatabase.ts new file mode 100644 index 00000000..d058aef5 --- /dev/null +++ b/frontend/react/src/unit/databases/aircraftdatabase.ts @@ -0,0 +1,37 @@ +import { getApp } from "../../olympusapp"; +import { GAME_MASTER } from "../../constants/constants"; +import { UnitDatabase } from "./unitdatabase" + +export class AircraftDatabase extends UnitDatabase { + constructor() { + super('api/databases/units/aircraftdatabase'); + } + + getCategory() { + return "Aircraft"; + } + + getSpawnPointsByName(name: string) { + if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns) + return 0; + + const blueprint = this.getByName(name); + if (blueprint?.cost != undefined) + return blueprint?.cost; + + if (blueprint?.era == "WW2") + return 20; + else if (blueprint?.era == "Early Cold War") + return 50; + else if (blueprint?.era == "Mid Cold War") + return 100; + else if (blueprint?.era == "Late Cold War") + return 200; + else if (blueprint?.era == "Modern") + return 400; + return 0; + } +} + +export var aircraftDatabase = new AircraftDatabase(); + diff --git a/frontend/react/src/unit/databases/citiesdatabase.ts b/frontend/react/src/unit/databases/citiesdatabase.ts new file mode 100644 index 00000000..5979ff28 --- /dev/null +++ b/frontend/react/src/unit/databases/citiesdatabase.ts @@ -0,0 +1,7137 @@ +export var citiesDatabase: {lat: number, lng: number, pop: number}[] = [ + { + lat: 41.0136, + lng: 28.955, + pop: 16079000, + }, + { + lat: 39.93, + lng: 32.85, + pop: 5503985, + }, + { + lat: 31.9497, + lng: 35.9328, + pop: 4007526, + }, + { + lat: 25.2631, + lng: 55.2972, + pop: 3331420, + }, + { + lat: 29.3697, + lng: 47.9783, + pop: 3000000, + }, + { + lat: 40.1833, + lng: 29.05, + pop: 2901396, + }, + { + lat: 36.8874, + lng: 30.7075, + pop: 2426356, + }, + { + lat: 32.6447, + lng: 51.6675, + pop: 2219343, + }, + { + lat: 36.2333, + lng: -115.2654, + pop: 2150373, + }, + { + lat: 37.0628, + lng: 37.3792, + pop: 2028563, + }, + { + lat: 37.1583, + lng: 38.7917, + pop: 1985753, + }, + { + lat: 36.2, + lng: 37.16, + pop: 1916781, + }, + { + lat: 36.8, + lng: 34.6333, + pop: 1814468, + }, + { + lat: 37, + lng: 35.3213, + pop: 1765981, + }, + { + lat: 33.5131, + lng: 36.2919, + pop: 1754000, + }, + { + lat: 29.61, + lng: 52.5425, + pop: 1565572, + }, + { + lat: 24.4667, + lng: 54.3667, + pop: 1483000, + }, + { + lat: 23.6139, + lng: 58.5922, + pop: 1421409, + }, + { + lat: 32.08, + lng: 34.78, + pop: 1388400, + }, + { + lat: 41.2903, + lng: 36.3336, + pop: 1335716, + }, + { + lat: 30.515, + lng: 47.81, + pop: 1326564, + }, + { + lat: 31.3203, + lng: 48.6692, + pop: 1261042, + }, + { + lat: 25.3575, + lng: 55.3908, + pop: 1247749, + }, + { + lat: 41.0167, + lng: 39.55, + pop: 1215351, + }, + { + lat: 25.2867, + lng: 51.5333, + pop: 1186023, + }, + { + lat: 47.2333, + lng: 39.7, + pop: 1137704, + }, + { + lat: 41.7225, + lng: 44.7925, + pop: 1118035, + }, + { + lat: 40.1814, + lng: 44.5144, + pop: 1075800, + }, + { + lat: 46.4775, + lng: 30.7326, + pop: 1017699, + }, + { + lat: 47.0228, + lng: 28.8353, + pop: 702300, + }, + { + lat: 26.225, + lng: 50.5775, + pop: 436000, + }, + { + lat: 33.8869, + lng: 35.5131, + pop: 361366, + }, + { + lat: 35.1725, + lng: 33.365, + pop: 330000, + }, + { + lat: 15.2137, + lng: 145.7546, + pop: 2500, + }, + { + lat: 13.4745, + lng: 144.7504, + pop: 1051, + }, + { + lat: 45.0333, + lng: 38.9667, + pop: 948827, + }, + { + lat: 39.9244, + lng: 32.8856, + pop: 914501, + }, + { + lat: 41.047, + lng: 28.658, + pop: 891120, + }, + { + lat: 26.4333, + lng: 50.1, + pop: 903312, + }, + { + lat: 39.7767, + lng: 30.5206, + pop: 871187, + }, + { + lat: 36.9831, + lng: 35.3328, + pop: 792536, + }, + { + lat: 41.0344, + lng: 28.8564, + pop: 734369, + }, + { + lat: 34.7333, + lng: 36.7167, + pop: 775404, + }, + { + lat: 41, + lng: 28.8, + pop: 770317, + }, + { + lat: 39.9086, + lng: 41.2769, + pop: 767848, + }, + { + lat: 24.2075, + lng: 55.7447, + pop: 766936, + }, + { + lat: 30.2833, + lng: 57.0833, + pop: 738374, + }, + { + lat: 35.5167, + lng: 35.7833, + pop: 700000, + }, + { + lat: 35.1333, + lng: 36.75, + pop: 696863, + }, + { + lat: 40.8747, + lng: 29.235, + pop: 693599, + }, + { + lat: 25.3833, + lng: 49.5833, + pop: 660788, + }, + { + lat: 40.11, + lng: 29.0821, + pop: 643681, + }, + { + lat: 25.25, + lng: 51.4, + pop: 605712, + }, + { + lat: 32.8192, + lng: 34.9992, + pop: 600000, + }, + { + lat: 42.9833, + lng: 47.4833, + pop: 592976, + }, + { + lat: 35.3529, + lng: -119.0359, + pop: 590845, + }, + { + lat: 29.4964, + lng: 60.8628, + pop: 587730, + }, + { + lat: 32.3399, + lng: 36.2052, + pop: 580000, + }, + { + lat: 32.55, + lng: 35.85, + pop: 569068, + }, + { + lat: 34.4367, + lng: 35.8344, + pop: 530000, + }, + { + lat: 46.35, + lng: 48.035, + pop: 532504, + }, + { + lat: 31.8822, + lng: 54.3397, + pop: 529673, + }, + { + lat: 27.1833, + lng: 56.2667, + pop: 526648, + }, + { + lat: 26.556, + lng: 49.996, + pop: 524182, + }, + { + lat: 44.605, + lng: 33.5225, + pop: 522057, + }, + { + lat: 46.975, + lng: 31.995, + pop: 498748, + }, + { + lat: 32.3436, + lng: 62.1194, + pop: 500000, + }, + { + lat: 25.4136, + lng: 55.4456, + pop: 490035, + }, + { + lat: 32.0833, + lng: 36.1, + pop: 481300, + }, + { + lat: 40.8872, + lng: 29.19, + pop: 461155, + }, + { + lat: 41.0719, + lng: 28.9664, + pop: 437026, + }, + { + lat: 41.0339, + lng: 28.8903, + pop: 444561, + }, + { + lat: 45.05, + lng: 41.9833, + pop: 450680, + }, + { + lat: 47.0958, + lng: 37.5494, + pop: 449498, + }, + { + lat: 40.9792, + lng: 28.7214, + pop: 435625, + }, + { + lat: 37.5833, + lng: 36.9333, + pop: 443575, + }, + { + lat: 41.005, + lng: 39.7225, + pop: 426882, + }, + { + lat: 43.5853, + lng: 39.7203, + pop: 411524, + }, + { + lat: 36.9981, + lng: 35.3439, + pop: 407054, + }, + { + lat: 29.35, + lng: 47.6833, + pop: 393432, + }, + { + lat: 27, + lng: 49.6544, + pop: 392948, + }, + { + lat: 34.5277, + lng: -117.3536, + pop: 389060, + }, + { + lat: 23.5333, + lng: 58.3833, + pop: 383257, + }, + { + lat: 34.6935, + lng: -118.1753, + pop: 381732, + }, + { + lat: 36.2025, + lng: 36.1606, + pop: 377793, + }, + { + lat: 39.75, + lng: 37.0167, + pop: 377561, + }, + { + lat: 40.8, + lng: 29.4333, + pop: 371000, + }, + { + lat: 30.3392, + lng: 48.3042, + pop: 370180, + }, + { + lat: 30.3833, + lng: 47.7, + pop: 370000, + }, + { + lat: 40.7625, + lng: 29.9175, + pop: 363416, + }, + { + lat: 43.2167, + lng: 27.9167, + pop: 348668, + }, + { + lat: 44.9484, + lng: 34.1, + pop: 341799, + }, + { + lat: 41.1669, + lng: 29.0572, + pop: 342503, + }, + { + lat: 36.9165, + lng: 34.8951, + pop: 339676, + }, + { + lat: 40.9683, + lng: 29.2617, + pop: 327798, + }, + { + lat: 41.0011, + lng: 28.6419, + pop: 331525, + }, + { + lat: 40.6828, + lng: 46.3606, + pop: 331400, + }, + { + lat: 39.6333, + lng: 27.8833, + pop: 331788, + }, + { + lat: 41.0225, + lng: 28.8717, + pop: 289331, + }, + { + lat: 36.55, + lng: 32, + pop: 312319, + }, + { + lat: 36.0133, + lng: -115.0381, + pop: 311250, + }, + { + lat: 43.04, + lng: 44.6775, + pop: 306978, + }, + { + lat: 35.95, + lng: 39.01, + pop: 299824, + }, + { + lat: 36.5817, + lng: 36.165, + pop: 297943, + }, + { + lat: 25.4416, + lng: 49.6642, + pop: 298562, + }, + { + lat: 40.5455, + lng: 34.957, + pop: 294807, + }, + { + lat: 43.3125, + lng: 45.6986, + pop: 291687, + }, + { + lat: 46.6425, + lng: 32.625, + pop: 291428, + }, + { + lat: 32.0178, + lng: 36.0464, + pop: 280000, + }, + { + lat: 44.1667, + lng: 28.6333, + pop: 283872, + }, + { + lat: 41.15, + lng: 27.8, + pop: 279251, + }, + { + lat: 31.5831, + lng: 64.3692, + pop: 276831, + }, + { + lat: 34.4175, + lng: -118.4964, + pop: 275230, + }, + { + lat: 44.7167, + lng: 37.75, + pop: 273278, + }, + { + lat: 40.7833, + lng: 30.4, + pop: 271515, + }, + { + lat: 35.3333, + lng: 40.15, + pop: 271800, + }, + { + lat: 41.1856, + lng: 28.7406, + pop: 270549, + }, + { + lat: 43.4833, + lng: 43.6167, + pop: 265162, + }, + { + lat: 40.0806, + lng: 29.5097, + pop: 268155, + }, + { + lat: 32.3825, + lng: 48.4019, + pop: 264709, + }, + { + lat: 37.075, + lng: 36.25, + pop: 264373, + }, + { + lat: 36.2883, + lng: -115.0888, + pop: 259638, + }, + { + lat: 40.8161, + lng: 29.3006, + pop: 255468, + }, + { + lat: 31.95, + lng: 34.8, + pop: 249860, + }, + { + lat: 41.0369, + lng: 29.1786, + pop: 251937, + }, + { + lat: 47.2167, + lng: 38.9167, + pop: 250287, + }, + { + lat: 37.1939, + lng: 40.5861, + pop: 252656, + }, + { + lat: 45.4233, + lng: 28.0425, + pop: 249432, + }, + { + lat: 36.3276, + lng: -119.3269, + pop: 249804, + }, + { + lat: 41.02, + lng: 28.5775, + pop: 247736, + }, + { + lat: 32.7003, + lng: 51.5211, + pop: 247128, + }, + { + lat: 41.1342, + lng: 29.0922, + pop: 246700, + }, + { + lat: 32.0889, + lng: 34.8864, + pop: 236169, + }, + { + lat: 40.8417, + lng: 31.1583, + pop: 240633, + }, + { + lat: 30.4411, + lng: 47.9725, + pop: 240300, + }, + { + lat: 34.6747, + lng: 33.0442, + pop: 235056, + }, + { + lat: 23.6703, + lng: 58.1891, + pop: 237816, + }, + { + lat: 32.6347, + lng: 51.3653, + pop: 235281, + }, + { + lat: 36.7833, + lng: 31.4333, + pop: 230597, + }, + { + lat: 40.9833, + lng: 37.8833, + pop: 229214, + }, + { + lat: 28.9264, + lng: 50.8514, + pop: 223504, + }, + { + lat: 26.2833, + lng: 50.2, + pop: 219679, + }, + { + lat: 36.0091, + lng: -115.2278, + pop: 219566, + }, + { + lat: 32.3286, + lng: 34.8567, + pop: 217244, + }, + { + lat: 36.0952, + lng: -115.2636, + pop: 217441, + }, + { + lat: 23.6167, + lng: 58.5667, + pop: 214901, + }, + { + lat: 33.5606, + lng: 35.3758, + pop: 200000, + }, + { + lat: 33.8936, + lng: 35.5403, + pop: 150000, + }, + { + lat: 32.0833, + lng: 34.8333, + pop: 193774, + }, + { + lat: 40.7347, + lng: 31.6075, + pop: 205525, + }, + { + lat: 32.0167, + lng: 34.7667, + pop: 194300, + }, + { + lat: 32.8667, + lng: 59.2167, + pop: 203636, + }, + { + lat: 25.69, + lng: 51.51, + pop: 202031, + }, + { + lat: 40.3139, + lng: 36.5542, + pop: 201294, + }, + { + lat: 34.4285, + lng: -119.7202, + pop: 198240, + }, + { + lat: 36.1783, + lng: -115.0487, + pop: 196411, + }, + { + lat: 37.2306, + lng: 39.7653, + pop: 195910, + }, + { + lat: 37.1847, + lng: 38.7908, + pop: 195552, + }, + { + lat: 37.1819, + lng: 33.2181, + pop: 194018, + }, + { + lat: 39.8417, + lng: 33.5139, + pop: 193093, + }, + { + lat: 45, + lng: 41.1167, + pop: 190709, + }, + { + lat: 32.3311, + lng: 50.8594, + pop: 190441, + }, + { + lat: 36.0872, + lng: -115.1355, + pop: 189852, + }, + { + lat: 36.5117, + lng: 40.7422, + pop: 188160, + }, + { + lat: 37.05, + lng: 41.22, + pop: 184231, + }, + { + lat: 45.2692, + lng: 27.9575, + pop: 180302, + }, + { + lat: 41.2792, + lng: 31.4208, + pop: 175605, + }, + { + lat: 29.437, + lng: 55.6802, + pop: 175000, + }, + { + lat: 33.2708, + lng: 35.1961, + pop: 160000, + }, + { + lat: 32.8667, + lng: 51.5667, + pop: 173329, + }, + { + lat: 41.6458, + lng: 41.6417, + pop: 169095, + }, + { + lat: 32.07, + lng: 34.8236, + pop: 159200, + }, + { + lat: 23.8494, + lng: 57.4386, + pop: 170000, + }, + { + lat: 31.8667, + lng: 36, + pop: 169434, + }, + { + lat: 34.5944, + lng: -118.1057, + pop: 167987, + }, + { + lat: 41.2833, + lng: 28, + pop: 166789, + }, + { + lat: 40.7833, + lng: 29.7333, + pop: 165503, + }, + { + lat: 35.9333, + lng: 36.6333, + pop: 165000, + }, + { + lat: 29.1322, + lng: 48.1261, + pop: 164212, + }, + { + lat: 40.6667, + lng: 29.8333, + pop: 162584, + }, + { + lat: 30.5589, + lng: 49.1981, + pop: 162797, + }, + { + lat: 30.4067, + lng: 55.9939, + pop: 161909, + }, + { + lat: 25.3722, + lng: 51.2047, + pop: 161240, + }, + { + lat: 30.96, + lng: 61.86, + pop: 160902, + }, + { + lat: 37.0289, + lng: 35.8125, + pop: 160474, + }, + { + lat: 32.2222, + lng: 35.2611, + pop: 156906, + }, + { + lat: 39.7464, + lng: 39.4914, + pop: 157452, + }, + { + lat: 32.5589, + lng: 36.0147, + pop: 155693, + }, + { + lat: 34.447, + lng: 35.8178, + pop: 150000, + }, + { + lat: 29.2694, + lng: 51.22, + pop: 155567, + }, + { + lat: 41.6344, + lng: 32.3375, + pop: 155016, + }, + { + lat: 46.8489, + lng: 35.3675, + pop: 154992, + }, + { + lat: 40.35, + lng: 27.9667, + pop: 154359, + }, + { + lat: 26.65, + lng: 50.1667, + pop: 153933, + }, + { + lat: 24.362, + lng: 56.7344, + pop: 151349, + }, + { + lat: 45.3619, + lng: 36.4711, + pop: 149566, + }, + { + lat: 42.25, + lng: 42.7, + pop: 147900, + }, + { + lat: 39.7186, + lng: 43.0508, + pop: 149188, + }, + { + lat: 32.3325, + lng: 35.7517, + pop: 148870, + }, + { + lat: 40.65, + lng: 35.8331, + pop: 149084, + }, + { + lat: 41.3764, + lng: 33.7764, + pop: 148931, + }, + { + lat: 29.8742, + lng: 52.8025, + pop: 148858, + }, + { + lat: 26.0031, + lng: 63.0544, + pop: 147791, + }, + { + lat: 44.05, + lng: 43.0667, + pop: 145836, + }, + { + lat: 32.6189, + lng: 36.1021, + pop: 146481, + }, + { + lat: 40.6556, + lng: 29.275, + pop: 144407, + }, + { + lat: 37.5167, + lng: 34.05, + pop: 145389, + }, + { + lat: 43.25, + lng: 46.5833, + pop: 141259, + }, + { + lat: 33.4472, + lng: 36.3361, + pop: 136427, + }, + { + lat: 37.332, + lng: 42.187, + pop: 143124, + }, + { + lat: 37.01, + lng: 37.7972, + pop: 142389, + }, + { + lat: 44.6, + lng: 40.0833, + pop: 141970, + }, + { + lat: 41.0247, + lng: 40.5222, + pop: 141143, + }, + { + lat: 28.5, + lng: 53.5606, + pop: 141634, + }, + { + lat: 41.1992, + lng: 36.7275, + pop: 138840, + }, + { + lat: 32.0167, + lng: 34.75, + pop: 128800, + }, + { + lat: 41.0736, + lng: 28.2478, + pop: 137861, + }, + { + lat: 39.9208, + lng: 44.0444, + pop: 137613, + }, + { + lat: 46.8403, + lng: 29.6433, + pop: 133807, + }, + { + lat: 40.9153, + lng: 38.3894, + pop: 135920, + }, + { + lat: 40.0528, + lng: 47.4614, + pop: 136000, + }, + { + lat: 32.46, + lng: 48.3592, + pop: 135116, + }, + { + lat: 32.0089, + lng: 51.8667, + pop: 134952, + }, + { + lat: 31.0286, + lng: 61.5011, + pop: 134950, + }, + { + lat: 33.8, + lng: 35.6, + pop: 130000, + }, + { + lat: 31.8981, + lng: 34.8081, + pop: 132671, + }, + { + lat: 33.4833, + lng: 36.35, + pop: 114363, + }, + { + lat: 30.4397, + lng: 48.1664, + pop: 133097, + }, + { + lat: 41.1986, + lng: 32.6264, + pop: 131989, + }, + { + lat: 41.5472, + lng: 45.0111, + pop: 128680, + }, + { + lat: 43.9167, + lng: 42.7167, + pop: 128779, + }, + { + lat: 37.45, + lng: 35.8, + pop: 130495, + }, + { + lat: 28.6781, + lng: 57.7406, + pop: 130429, + }, + { + lat: 40.8265, + lng: 29.3745, + pop: 129655, + }, + { + lat: 47.1667, + lng: 39.7333, + pop: 126769, + }, + { + lat: 33.464, + lng: 36.3044, + pop: 101827, + }, + { + lat: 37.0758, + lng: -113.5752, + pop: 127890, + }, + { + lat: 42.8803, + lng: 47.6383, + pop: 123988, + }, + { + lat: 29.1061, + lng: 58.3569, + pop: 127396, + }, + { + lat: 41.1333, + lng: 37.2833, + pop: 126702, + }, + { + lat: 33.5711, + lng: 36.4011, + pop: 123494, + }, + { + lat: 40.7894, + lng: 43.8475, + pop: 121976, + }, + { + lat: 36.9167, + lng: 31.1, + pop: 124335, + }, + { + lat: 37.3697, + lng: 36.1, + pop: 124053, + }, + { + lat: 44.2222, + lng: 42.0575, + pop: 122395, + }, + { + lat: 42.05, + lng: 48.3, + pop: 123720, + }, + { + lat: 30.5958, + lng: 50.2417, + pop: 122604, + }, + { + lat: 13.4692, + lng: 144.7332, + pop: 122411, + }, + { + lat: 36.085, + lng: 35.9806, + pop: 121109, + }, + { + lat: 41.4564, + lng: 31.7986, + pop: 120395, + }, + { + lat: 23.3908, + lng: 57.4244, + pop: 120000, + }, + { + lat: 43.2167, + lng: 44.7667, + pop: 117936, + }, + { + lat: 36.3761, + lng: 33.9322, + pop: 119303, + }, + { + lat: 44.6333, + lng: 41.9333, + pop: 117446, + }, + { + lat: 36.8278, + lng: -119.683, + pop: 118488, + }, + { + lat: 40.7333, + lng: 29.9667, + pop: 118066, + }, + { + lat: 41.3333, + lng: 27.9667, + pop: 116882, + }, + { + lat: 33.9697, + lng: 35.6156, + pop: 102221, + }, + { + lat: 41.0333, + lng: 37.5, + pop: 116154, + }, + { + lat: 25.7667, + lng: 55.95, + pop: 115949, + }, + { + lat: 36.7167, + lng: 37.1167, + pop: 116034, + }, + { + lat: 26.475, + lng: 50.0417, + pop: 115000, + }, + { + lat: 40.6078, + lng: 43.0958, + pop: 115891, + }, + { + lat: 46.7556, + lng: 36.7889, + pop: 114205, + }, + { + lat: 44.0333, + lng: 42.85, + pop: 113056, + }, + { + lat: 22.5667, + lng: 58.1167, + pop: 115040, + }, + { + lat: 31.5231, + lng: 49.8861, + pop: 114343, + }, + { + lat: 27.2025, + lng: 60.6847, + pop: 113750, + }, + { + lat: 37.4167, + lng: 41.3697, + pop: 113367, + }, + { + lat: 36.7108, + lng: 38.9478, + pop: 113194, + }, + { + lat: 33.8333, + lng: 35.9167, + pop: 100000, + }, + { + lat: 24.1456, + lng: 49.0653, + pop: 111214, + }, + { + lat: 28.9383, + lng: 53.6483, + pop: 110825, + }, + { + lat: 35.0118, + lng: 37.0525, + pop: 110683, + }, + { + lat: 45.1939, + lng: 33.3681, + pop: 108248, + }, + { + lat: 30.6683, + lng: 51.5881, + pop: 108505, + }, + { + lat: 30.0342, + lng: 47.9294, + pop: 107620, + }, + { + lat: 32.1714, + lng: 34.9083, + pop: 100800, + }, + { + lat: 39.5736, + lng: -119.7161, + pop: 106900, + }, + { + lat: 36.0778, + lng: 37.3733, + pop: 106460, + }, + { + lat: 25.2919, + lng: 60.6431, + pop: 106739, + }, + { + lat: 40.77, + lng: 47.0489, + pop: 106100, + }, + { + lat: 37.075, + lng: 41.2153, + pop: 105856, + }, + { + lat: 39.8208, + lng: 34.8083, + pop: 105167, + }, + { + lat: 29.9758, + lng: 48.4722, + pop: 105080, + }, + { + lat: 36.9764, + lng: 38.4269, + pop: 104302, + }, + { + lat: 46.3167, + lng: 44.2667, + pop: 103535, + }, + { + lat: 32.0456, + lng: 48.8567, + pop: 101878, + }, + { + lat: 23.2325, + lng: 56.4973, + pop: 101640, + }, + { + lat: 31.9364, + lng: 49.3039, + pop: 100497, + }, + { + lat: 32.1653, + lng: 34.8458, + pop: 93989, + }, + { + lat: 40.5986, + lng: 33.6192, + pop: 96025, + }, + { + lat: 40.8128, + lng: 44.4883, + pop: 90525, + }, + { + lat: 25.1223, + lng: 56.3342, + pop: 93673, + }, + { + lat: 46.8333, + lng: 29.4833, + pop: 91882, + }, + { + lat: 43.5667, + lng: 27.8333, + pop: 90419, + }, + { + lat: 34.8833, + lng: 35.8833, + pop: 89457, + }, + { + lat: 32.0333, + lng: 35.7333, + pop: 88900, + }, + { + lat: 33.3833, + lng: 35.45, + pop: 80000, + }, + { + lat: 25.18, + lng: 51.61, + pop: 87970, + }, + { + lat: 33.45, + lng: 36.25, + pop: 84044, + }, + { + lat: 37.3131, + lng: 40.735, + pop: 86948, + }, + { + lat: 34.9167, + lng: 33.6333, + pop: 84900, + }, + { + lat: 35.8367, + lng: 38.5481, + pop: 84000, + }, + { + lat: 32.7019, + lng: 35.3033, + pop: 83400, + }, + { + lat: 40.1431, + lng: 29.9792, + pop: 81723, + }, + { + lat: 32.1833, + lng: 34.8667, + pop: 74000, + }, + { + lat: 26.25, + lng: 50.6167, + pop: 75000, + }, + { + lat: 31.9275, + lng: 34.8625, + pop: 75500, + }, + { + lat: 45.19, + lng: 28.8, + pop: 73707, + }, + { + lat: 39.9323, + lng: 48.9203, + pop: 70684, + }, + { + lat: 22.9333, + lng: 57.5333, + pop: 72076, + }, + { + lat: 32.0714, + lng: 34.81, + pop: 59518, + }, + { + lat: 33.2631, + lng: 35.2389, + pop: 61973, + }, + { + lat: 44.15, + lng: 43.4667, + pop: 67054, + }, + { + lat: 24.2592, + lng: 55.7839, + pop: 67963, + }, + { + lat: 41.2, + lng: 47.1667, + pop: 68360, + }, + { + lat: 42.8167, + lng: 47.1167, + pop: 65080, + }, + { + lat: 40.2597, + lng: 40.2278, + pop: 66633, + }, + { + lat: 34.7667, + lng: 32.4167, + pop: 63600, + }, + { + lat: 32.7125, + lng: 36.5667, + pop: 64730, + }, + { + lat: 43, + lng: 41.0167, + pop: 64441, + }, + { + lat: 46.6383, + lng: 27.7292, + pop: 63035, + }, + { + lat: 46.3017, + lng: 30.6569, + pop: 59800, + }, + { + lat: 26.219, + lng: 50.538, + pop: 44769, + }, + { + lat: 32.15, + lng: 34.8833, + pop: 56659, + }, + { + lat: 40.6172, + lng: 47.15, + pop: 59036, + }, + { + lat: 32.8, + lng: 35.1, + pop: 55464, + }, + { + lat: 32.0956, + lng: 34.9567, + pop: 56300, + }, + { + lat: 46.2167, + lng: 27.6667, + pop: 55837, + }, + { + lat: 33.4728, + lng: 36.3344, + pop: 50880, + }, + { + lat: 39.1511, + lng: -119.7476, + pop: 57957, + }, + { + lat: 32.5194, + lng: 35.1536, + pop: 55300, + }, + { + lat: 40.4597, + lng: 39.4778, + pop: 57269, + }, + { + lat: 26.1128, + lng: 50.5139, + pop: 52700, + }, + { + lat: 32.1903, + lng: 34.9686, + pop: 51683, + }, + { + lat: 31.9333, + lng: 34.8, + pop: 50200, + }, + { + lat: 33.5186, + lng: 35.3661, + pop: 50000, + }, + { + lat: 40.5, + lng: 44.7667, + pop: 52808, + }, + { + lat: 32.0636, + lng: 34.8553, + pop: 41900, + }, + { + lat: 33.5667, + lng: 36.3667, + pop: 45974, + }, + { + lat: 32.2723, + lng: 35.8914, + pop: 50745, + }, + { + lat: 41.9817, + lng: 44.1124, + pop: 48143, + }, + { + lat: 40.2739, + lng: 44.6256, + pop: 44400, + }, + { + lat: 32.0522, + lng: 34.9511, + pop: 46896, + }, + { + lat: 34.1236, + lng: 35.6511, + pop: 40000, + }, + { + lat: 32.15, + lng: 34.8333, + pop: 46700, + }, + { + lat: 26.1736, + lng: 50.5478, + pop: 40000, + }, + { + lat: 32.9136, + lng: 35.2961, + pop: 45300, + }, + { + lat: 31.8558, + lng: 34.73, + pop: 42314, + }, + { + lat: 36.1008, + lng: -115.0379, + pop: 45105, + }, + { + lat: 42.5083, + lng: 41.8667, + pop: 42998, + }, + { + lat: 32.8333, + lng: 35.0833, + pop: 39900, + }, + { + lat: 25.5533, + lng: 55.5475, + pop: 44411, + }, + { + lat: 32.0333, + lng: 34.85, + pop: 36706, + }, + { + lat: 32.8056, + lng: 35.1694, + pop: 41600, + }, + { + lat: 40.15, + lng: 44.04, + pop: 38635, + }, + { + lat: 41.1111, + lng: 42.7022, + pop: 42226, + }, + { + lat: 34.4, + lng: 35.9, + pop: 30000, + }, + { + lat: 35.125, + lng: 33.9417, + pop: 40920, + }, + { + lat: 40.8297, + lng: 46.0189, + pop: 40600, + }, + { + lat: 41.4708, + lng: 48.8097, + pop: 39900, + }, + { + lat: 40.3744, + lng: 47.1267, + pop: 38500, + }, + { + lat: 33.1256, + lng: 35.8239, + pop: 37022, + }, + { + lat: 40.6531, + lng: 47.7406, + pop: 36200, + }, + { + lat: 36.1365, + lng: -115.137, + pop: 36307, + }, + { + lat: 32.0333, + lng: 34.8833, + pop: 29146, + }, + { + lat: 41.0933, + lng: 45.3661, + pop: 35102, + }, + { + lat: 41.1833, + lng: 41.8181, + pop: 35081, + }, + { + lat: 32.0781, + lng: 34.8475, + pop: 25298, + }, + { + lat: 22.6833, + lng: 58.55, + pop: 35000, + }, + { + lat: 33.92, + lng: 35.67, + pop: 28000, + }, + { + lat: 25.2525, + lng: 51.5592, + pop: 29703, + }, + { + lat: 35.3403, + lng: 33.3192, + pop: 33207, + }, + { + lat: 33.1258, + lng: 35.4428, + pop: 30000, + }, + { + lat: 42.1167, + lng: 48.1833, + pop: 29716, + }, + { + lat: 41.6336, + lng: 46.6433, + pop: 31300, + }, + { + lat: 39.8697, + lng: 48.06, + pop: 31310, + }, + { + lat: 40.0128, + lng: 48.4789, + pop: 30918, + }, + { + lat: 47.2167, + lng: 27.8167, + pop: 30804, + }, + { + lat: 32.3171, + lng: 34.9358, + pop: 28025, + }, + { + lat: 33.5, + lng: 36.3667, + pop: 22535, + }, + { + lat: 45.9167, + lng: 28.1836, + pop: 30018, + }, + { + lat: 40.4097, + lng: 44.6431, + pop: 25039, + }, + { + lat: 40.65, + lng: 47.4761, + pop: 29600, + }, + { + lat: 35.3683, + lng: -118.9225, + pop: 29110, + }, + { + lat: 32.2322, + lng: 34.9483, + pop: 26200, + }, + { + lat: 26.12, + lng: 50.65, + pop: 20000, + }, + { + lat: 40.5905, + lng: 46.3239, + pop: 25758, + }, + { + lat: 34.0063, + lng: 36.2073, + pop: 24000, + }, + { + lat: 32.1151, + lng: 34.9751, + pop: 21848, + }, + { + lat: 33.8333, + lng: 35.5333, + pop: 9000, + }, + { + lat: 32.2822, + lng: 34.9833, + pop: 21451, + }, + { + lat: 39.9539, + lng: 44.5506, + pop: 21300, + }, + { + lat: 40.2975, + lng: 44.3617, + pop: 21600, + }, + { + lat: 40.8792, + lng: 45.1472, + pop: 20509, + }, + { + lat: 40.0433, + lng: 48.9356, + pop: 21504, + }, + { + lat: 47.3833, + lng: 28.8167, + pop: 21065, + }, + { + lat: 26.1497, + lng: 50.4653, + pop: 18000, + }, + { + lat: 26.2186, + lng: 50.4756, + pop: 18000, + }, + { + lat: 40.3589, + lng: 45.1267, + pop: 20765, + }, + { + lat: 41.3597, + lng: 48.5125, + pop: 20791, + }, + { + lat: 40.79, + lng: 48.1519, + pop: 20660, + }, + { + lat: 40.5692, + lng: 48.4008, + pop: 20200, + }, + { + lat: 41.1189, + lng: 45.4539, + pop: 20200, + }, + { + lat: 46.3167, + lng: 28.6667, + pop: 20113, + }, + { + lat: 34.2264, + lng: 35.66, + pop: 9613, + }, + { + lat: 33.4292, + lng: 36.3611, + pop: 12330, + }, + { + lat: 33.8219, + lng: 35.5875, + pop: 13000, + }, + { + lat: 41.9167, + lng: 45.4833, + pop: 19629, + }, + { + lat: 41.0761, + lng: 49.1139, + pop: 18655, + }, + { + lat: 34.7024, + lng: 33.0453, + pop: 14477, + }, + { + lat: 47.1333, + lng: 28.6167, + pop: 18376, + }, + { + lat: 39.9311, + lng: 48.3697, + pop: 17900, + }, + { + lat: 26.1008, + lng: 50.4878, + pop: 14800, + }, + { + lat: 26.1833, + lng: 56.25, + pop: 17777, + }, + { + lat: 41.4264, + lng: 48.4356, + pop: 16500, + }, + { + lat: 46.6333, + lng: 29.4, + pop: 15939, + }, + { + lat: 40.3383, + lng: 48.1608, + pop: 15385, + }, + { + lat: 26.2306, + lng: 50.5108, + pop: 12000, + }, + { + lat: 41.9406, + lng: 41.9906, + pop: 14785, + }, + { + lat: 36.6087, + lng: -119.5434, + pop: 14666, + }, + { + lat: 40.5183, + lng: 47.6542, + pop: 14273, + }, + { + lat: 40.3019, + lng: 44.5831, + pop: 10198, + }, + { + lat: 33.4517, + lng: 35.2908, + pop: 10965, + }, + { + lat: 41.6389, + lng: 42.9861, + pop: 14000, + }, + { + lat: 40.9922, + lng: 45.6289, + pop: 13700, + }, + { + lat: 42.0267, + lng: 35.1511, + pop: 13354, + }, + { + lat: 46.95, + lng: 28.7833, + pop: 12515, + }, + { + lat: 33.2814, + lng: 35.3964, + pop: 10000, + }, + { + lat: 46.8167, + lng: 28.5833, + pop: 12491, + }, + { + lat: 40.345, + lng: 46.9289, + pop: 12563, + }, + { + lat: 45.9, + lng: 28.6689, + pop: 12355, + }, + { + lat: 39.7756, + lng: 47.6186, + pop: 12263, + }, + { + lat: 46.5167, + lng: 28.7833, + pop: 11997, + }, + { + lat: 40.3147, + lng: 44.5936, + pop: 9513, + }, + { + lat: 41.4225, + lng: 46.9242, + pop: 11415, + }, + { + lat: 35.379, + lng: -118.9578, + pop: 11443, + }, + { + lat: 43.1667, + lng: 44.8, + pop: 10333, + }, + { + lat: 35.3832, + lng: -118.9743, + pop: 11025, + }, + { + lat: 46.8833, + lng: 29.2167, + pop: 10872, + }, + { + lat: 47.25, + lng: 28.3, + pop: 10808, + }, + { + lat: 40.5244, + lng: 46.1069, + pop: 10700, + }, + { + lat: 35.3636, + lng: -118.965, + pop: 10517, + }, + { + lat: 40.5067, + lng: 46.825, + pop: 10100, + }, + { + lat: 40.77, + lng: 46.4111, + pop: 10228, + }, + { + lat: 47.0833, + lng: 28.1833, + pop: 10063, + }, + { + lat: 36.6211, + lng: -119.3188, + pop: 9680, + }, + { + lat: 40.5656, + lng: 45.8161, + pop: 8657, + }, + { + lat: 35.3972, + lng: -118.9892, + pop: 8726, + }, + { + lat: 46.3336, + lng: 28.9614, + pop: 8471, + }, + { + lat: 40.2183, + lng: 47.7083, + pop: 8450, + }, + { + lat: 41.7258, + lng: 46.4083, + pop: 8134, + }, + { + lat: 41.8464, + lng: 44.7194, + pop: 7940, + }, + { + lat: 39.7611, + lng: 45.3333, + pop: 7633, + }, + { + lat: 46.4833, + lng: 28.25, + pop: 7443, + }, + { + lat: 40.6103, + lng: 46.7897, + pop: 7400, + }, + { + lat: 40.1067, + lng: 46.0383, + pop: 7246, + }, + { + lat: 46.5153, + lng: 29.6631, + pop: 7078, + }, + { + lat: 41.0708, + lng: 47.4583, + pop: 6876, + }, + { + lat: 47.2167, + lng: 29.1667, + pop: 6708, + }, + { + lat: 34.5506, + lng: 36.0781, + pop: 4730, + }, + { + lat: 39.7953, + lng: 47.1131, + pop: 5700, + }, + { + lat: 39.7583, + lng: 46.7483, + pop: 4446, + }, + { + lat: 40.5367, + lng: 48.9328, + pop: 3945, + }, + { + lat: 39.9833, + lng: 46.9167, + pop: 3770, + }, + { + lat: 46.2667, + lng: 28.2167, + pop: 3429, + }, + { + lat: 39.6408, + lng: 46.5469, + pop: 2190, + }, + { + lat: 39.7203, + lng: 44.8531, + pop: 2000, + }, + { + lat: 42.5194, + lng: 43.15, + pop: 2047, + }, + { + lat: 40.9078, + lng: 49.0733, + pop: 1600, + }, + { + lat: 39.9111, + lng: 46.7892, + pop: 1397, + }, + { + lat: 36.2692, + lng: 36.5672, + pop: 98534, + }, + { + lat: 34.598, + lng: -112.3185, + pop: 97901, + }, + { + lat: 41.2719, + lng: 36.3508, + pop: 97564, + }, + { + lat: 32.45, + lng: 34.9167, + pop: 95700, + }, + { + lat: 41.5722, + lng: 35.9147, + pop: 97452, + }, + { + lat: 30.3586, + lng: 50.7981, + pop: 96728, + }, + { + lat: 29.6194, + lng: 51.6542, + pop: 96683, + }, + { + lat: 36.3274, + lng: -119.6549, + pop: 95459, + }, + { + lat: 40.6667, + lng: 36.5667, + pop: 95361, + }, + { + lat: 37.025, + lng: 37.9769, + pop: 95149, + }, + { + lat: 45.1333, + lng: 42.0333, + pop: 93658, + }, + { + lat: 41.4267, + lng: 32.0758, + pop: 91569, + }, + { + lat: 31.9077, + lng: 35.0076, + pop: 90013, + }, + { + lat: 25.1264, + lng: 62.3225, + pop: 90762, + }, + { + lat: 36.4128, + lng: 35.8867, + pop: 90456, + }, + { + lat: 35.6386, + lng: 36.6717, + pop: 90000, + }, + { + lat: 40.6833, + lng: 30.6253, + pop: 89301, + }, + { + lat: 36.3225, + lng: 41.8642, + pop: 88023, + }, + { + lat: 36.8461, + lng: 40.0489, + pop: 87684, + }, + { + lat: 32.6539, + lng: 51.7553, + pop: 86063, + }, + { + lat: 46.7111, + lng: 38.2733, + pop: 85760, + }, + { + lat: 22.968, + lng: 57.298, + pop: 85000, + }, + { + lat: 47.1, + lng: 39.4167, + pop: 80721, + }, + { + lat: 44.8667, + lng: 37.3667, + pop: 81447, + }, + { + lat: 32.2444, + lng: 54.0186, + pop: 80712, + }, + { + lat: 23.3, + lng: 57.9833, + pop: 80538, + }, + { + lat: 40.39, + lng: 36.09, + pop: 79916, + }, + { + lat: 44.4994, + lng: 34.17, + pop: 79458, + }, + { + lat: 39.8144, + lng: 35.1903, + pop: 79314, + }, + { + lat: 32.4797, + lng: 51.7753, + pop: 79023, + }, + { + lat: 30.4356, + lng: 49.1056, + pop: 78353, + }, + { + lat: 36.5275, + lng: 37.9553, + pop: 78255, + }, + { + lat: 45.4333, + lng: 40.5667, + pop: 78149, + }, + { + lat: 44.575, + lng: 38.0725, + pop: 77212, + }, + { + lat: 32.1942, + lng: 48.2436, + pop: 77148, + }, + { + lat: 35.35, + lng: 35.9167, + pop: 75505, + }, + { + lat: 37.341, + lng: 41.894, + pop: 76523, + }, + { + lat: 31.9519, + lng: 34.8881, + pop: 75700, + }, + { + lat: 34.5352, + lng: -117.2109, + pop: 75311, + }, + { + lat: 32.3061, + lng: 54.0081, + pop: 75271, + }, + { + lat: 39.9078, + lng: 30.0367, + pop: 74441, + }, + { + lat: 31.28, + lng: 49.6036, + pop: 74285, + }, + { + lat: 44.2167, + lng: 43.1333, + pop: 74141, + }, + { + lat: 34.3688, + lng: 41.0945, + pop: 74100, + }, + { + lat: 37.3914, + lng: 36.8522, + pop: 73770, + }, + { + lat: 37.6764, + lng: 31.7261, + pop: 73768, + }, + { + lat: 29.5839, + lng: 50.5189, + pop: 73472, + }, + { + lat: 36.85, + lng: 31.05, + pop: 73260, + }, + { + lat: 41.1417, + lng: 28.4631, + pop: 72966, + }, + { + lat: 40.875, + lng: 35.4633, + pop: 71916, + }, + { + lat: 45.3517, + lng: 28.8364, + pop: 71411, + }, + { + lat: 23.5242, + lng: 58.4989, + pop: 70000, + }, + { + lat: 45.0489, + lng: 35.3792, + pop: 69145, + }, + { + lat: 29.0769, + lng: 48.0838, + pop: 68763, + }, + { + lat: 36.1995, + lng: -119.34, + pop: 68395, + }, + { + lat: 36.0643, + lng: -119.0338, + pop: 67887, + }, + { + lat: 37.025, + lng: 36.6345, + pop: 67674, + }, + { + lat: 30.7458, + lng: 49.7086, + pop: 67427, + }, + { + lat: 40.9333, + lng: 38.2333, + pop: 66736, + }, + { + lat: 37.575, + lng: 32.7747, + pop: 66794, + }, + { + lat: 45.25, + lng: 38.1167, + pop: 66285, + }, + { + lat: 36.0243, + lng: 32.8026, + pop: 65920, + }, + { + lat: 28.4167, + lng: 48.5, + pop: 65000, + }, + { + lat: 37.4592, + lng: 30.5953, + pop: 64943, + }, + { + lat: 37.4183, + lng: 31.8506, + pop: 64687, + }, + { + lat: 32.8889, + lng: 36.0431, + pop: 63676, + }, + { + lat: 33.6, + lng: 36.3, + pop: 63554, + }, + { + lat: 40.5833, + lng: 36.9667, + pop: 64119, + }, + { + lat: 30.8128, + lng: 56.5639, + pop: 63744, + }, + { + lat: 44.7833, + lng: 44.1667, + pop: 62495, + }, + { + lat: 23.32, + lng: 58.908, + pop: 63133, + }, + { + lat: 36.6333, + lng: 33.4333, + pop: 62853, + }, + { + lat: 32.3464, + lng: 51.5044, + pop: 62454, + }, + { + lat: 32.3117, + lng: 35.0272, + pop: 61941, + }, + { + lat: 44.1044, + lng: 39.0772, + pop: 62269, + }, + { + lat: 34.9167, + lng: 36.7333, + pop: 61176, + }, + { + lat: 37.3658, + lng: 40.2697, + pop: 61830, + }, + { + lat: 33.0058, + lng: 35.0989, + pop: 60000, + }, + { + lat: 35.0025, + lng: 40.5117, + pop: 60780, + }, + { + lat: 27.8389, + lng: 52.0619, + pop: 60187, + }, + { + lat: 32.6064, + lng: 35.2881, + pop: 60000, + }, + { + lat: 44.6333, + lng: 40.7333, + pop: 60164, + }, + { + lat: 43.1333, + lng: 45.55, + pop: 59954, + }, + { + lat: 27.3708, + lng: 62.3342, + pop: 60114, + }, + { + lat: 43.75, + lng: 44.0333, + pop: 57883, + }, + { + lat: 46.1833, + lng: 30.35, + pop: 57210, + }, + { + lat: 42.5633, + lng: 47.8636, + pop: 58690, + }, + { + lat: 40.1728, + lng: 44.2925, + pop: 57500, + }, + { + lat: 39.9319, + lng: 48.9203, + pop: 58253, + }, + { + lat: 45.8667, + lng: 40.1333, + pop: 57771, + }, + { + lat: 46.4833, + lng: 41.5333, + pop: 57622, + }, + { + lat: 44.9233, + lng: 37.9806, + pop: 57229, + }, + { + lat: 31.9678, + lng: 51.2894, + pop: 57071, + }, + { + lat: 30.795, + lng: 50.5644, + pop: 57036, + }, + { + lat: 29.1489, + lng: 48.1057, + pop: 56554, + }, + { + lat: 28.2211, + lng: 61.2158, + pop: 56584, + }, + { + lat: 34.5006, + lng: -114.3113, + pop: 56510, + }, + { + lat: 36.7994, + lng: 36.5178, + pop: 56409, + }, + { + lat: 31.2414, + lng: 48.6525, + pop: 56252, + }, + { + lat: 43.15, + lng: 45.9, + pop: 56226, + }, + { + lat: 32.3897, + lng: 51.3767, + pop: 55984, + }, + { + lat: 35.4389, + lng: 36.6511, + pop: 55843, + }, + { + lat: 40.3, + lng: 35.8833, + pop: 55673, + }, + { + lat: 41.2667, + lng: 27.9667, + pop: 54268, + }, + { + lat: 25.25, + lng: 51.3732, + pop: 54339, + }, + { + lat: 43.35, + lng: 46.1, + pop: 52908, + }, + { + lat: 27.95, + lng: 57.7, + pop: 52624, + }, + { + lat: 34.0167, + lng: 36.7167, + pop: 52502, + }, + { + lat: 41.2125, + lng: 36.4569, + pop: 52258, + }, + { + lat: 35.7662, + lng: -119.2635, + pop: 52206, + }, + { + lat: 44.7686, + lng: 39.8733, + pop: 52082, + }, + { + lat: 34.4536, + lng: 40.9367, + pop: 52020, + }, + { + lat: 45.6167, + lng: 38.9333, + pop: 51925, + }, + { + lat: 30.1164, + lng: 55.1186, + pop: 51620, + }, + { + lat: 33.7986, + lng: 35.825, + pop: 50000, + }, + { + lat: 31.5608, + lng: 48.1831, + pop: 51431, + }, + { + lat: 34.56, + lng: 38.2672, + pop: 51323, + }, + { + lat: 43.85, + lng: 46.7167, + pop: 49247, + }, + { + lat: 34.1794, + lng: 36.4208, + pop: 50000, + }, + { + lat: 39.8153, + lng: 46.7519, + pop: 49848, + }, + { + lat: 32.9278, + lng: 35.0817, + pop: 48900, + }, + { + lat: 37.7147, + lng: 33.5508, + pop: 49766, + }, + { + lat: 40.9333, + lng: 40.05, + pop: 49496, + }, + { + lat: 41.4411, + lng: 27.9216, + pop: 49106, + }, + { + lat: 40.161, + lng: 34.377, + pop: 49082, + }, + { + lat: 35.0183, + lng: 40.4533, + pop: 48922, + }, + { + lat: 41.4333, + lng: 31.75, + pop: 48381, + }, + { + lat: 30.6497, + lng: 48.6647, + pop: 48642, + }, + { + lat: 44.8667, + lng: 40.6167, + pop: 48439, + }, + { + lat: 34.9833, + lng: 36.0833, + pop: 47982, + }, + { + lat: 40.1703, + lng: 31.9211, + pop: 48274, + }, + { + lat: 36.3, + lng: 30.15, + pop: 48131, + }, + { + lat: 23.3103, + lng: 57.9455, + pop: 47718, + }, + { + lat: 37.1042, + lng: 40.93, + pop: 47580, + }, + { + lat: 33.9667, + lng: 36.6667, + pop: 47136, + }, + { + lat: 34.5119, + lng: 36.5764, + pop: 46772, + }, + { + lat: 40.8333, + lng: 35.65, + pop: 46608, + }, + { + lat: 34.8333, + lng: 36.7333, + pop: 45853, + }, + { + lat: 34.25, + lng: 35.65, + pop: 45000, + }, + { + lat: 31.6033, + lng: 55.4003, + pop: 45453, + }, + { + lat: 47.25, + lng: 39.8667, + pop: 45078, + }, + { + lat: 46.755, + lng: 33.375, + pop: 45069, + }, + { + lat: 34.5849, + lng: -112.4473, + pop: 45063, + }, + { + lat: 32.6153, + lng: 51.5556, + pop: 43183, + }, + { + lat: 36.891, + lng: 38.3536, + pop: 44821, + }, + { + lat: 35.8126, + lng: 36.3176, + pop: 44322, + }, + { + lat: 41.1833, + lng: 31.3833, + pop: 44286, + }, + { + lat: 32.7944, + lng: 35.5333, + pop: 44200, + }, + { + lat: 41.088, + lng: 40.7232, + pop: 44304, + }, + { + lat: 30.8989, + lng: 52.6867, + pop: 44341, + }, + { + lat: 41.4653, + lng: 34.7708, + pop: 44004, + }, + { + lat: 25.935, + lng: 49.6661, + pop: 44000, + }, + { + lat: 32.4711, + lng: 34.9675, + pop: 42100, + }, + { + lat: 32.8333, + lng: 35.0833, + pop: 42000, + }, + { + lat: 35.1822, + lng: 35.9403, + pop: 43151, + }, + { + lat: 33.35, + lng: 36.2333, + pop: 43456, + }, + { + lat: 25.3603, + lng: 60.3994, + pop: 43732, + }, + { + lat: 32.2667, + lng: 35.0103, + pop: 43100, + }, + { + lat: 40.945, + lng: 40.2644, + pop: 43499, + }, + { + lat: 24.75, + lng: 56.4667, + pop: 43312, + }, + { + lat: 36.6, + lng: 30.55, + pop: 43226, + }, + { + lat: 39.6658, + lng: 35.8836, + pop: 42919, + }, + { + lat: 40.3381, + lng: 42.5731, + pop: 42683, + }, + { + lat: 36.2235, + lng: -115.9974, + pop: 42471, + }, + { + lat: 43.7333, + lng: 44.7, + pop: 42155, + }, + { + lat: 42.15, + lng: 41.6667, + pop: 41465, + }, + { + lat: 45.4686, + lng: 39.4519, + pop: 42019, + }, + { + lat: 36.3481, + lng: 37.5308, + pop: 41786, + }, + { + lat: 36.75, + lng: 36.2167, + pop: 41409, + }, + { + lat: 43.3167, + lng: 44.9167, + pop: 41469, + }, + { + lat: 36.9533, + lng: 36.2033, + pop: 41368, + }, + { + lat: 32.8333, + lng: 35.0667, + pop: 39416, + }, + { + lat: 45.2667, + lng: 37.3667, + pop: 41133, + }, + { + lat: 37.2489, + lng: 37.8658, + pop: 41142, + }, + { + lat: 41.19, + lng: 40.9831, + pop: 41084, + }, + { + lat: 35.1205, + lng: -114.5461, + pop: 41064, + }, + { + lat: 32.7997, + lng: 51.6956, + pop: 40945, + }, + { + lat: 33.775, + lng: 35.9, + pop: 40000, + }, + { + lat: 26.9581, + lng: 56.2719, + pop: 40678, + }, + { + lat: 28.6489, + lng: 51.3794, + pop: 40722, + }, + { + lat: 25.3333, + lng: 56.35, + pop: 39515, + }, + { + lat: 31.5081, + lng: 50.8319, + pop: 40528, + }, + { + lat: 39.9458, + lng: 41.1053, + pop: 40350, + }, + { + lat: 41.0172, + lng: 34.0383, + pop: 40245, + }, + { + lat: 35.8636, + lng: 36.8006, + pop: 39901, + }, + { + lat: 40.9667, + lng: 35.6667, + pop: 40194, + }, + { + lat: 44.2503, + lng: 28.2614, + pop: 39780, + }, + { + lat: 43.6825, + lng: 43.5339, + pop: 38192, + }, + { + lat: 32.4611, + lng: 35.3, + pop: 39004, + }, + { + lat: 33.8067, + lng: 36.7403, + pop: 39903, + }, + { + lat: 26.5578, + lng: 54.0194, + pop: 39853, + }, + { + lat: 44.4667, + lng: 39.7333, + pop: 39762, + }, + { + lat: 40.9667, + lng: 39.9, + pop: 39624, + }, + { + lat: 31.9, + lng: 35.2, + pop: 38998, + }, + { + lat: 40.6603, + lng: 29.3236, + pop: 39110, + }, + { + lat: 44.8667, + lng: 38.15, + pop: 39374, + }, + { + lat: 37.1667, + lng: 42.1333, + pop: 39000, + }, + { + lat: 39.9139, + lng: 28.1603, + pop: 39058, + }, + { + lat: 41.0833, + lng: 31.1167, + pop: 38846, + }, + { + lat: 41.5097, + lng: 34.2142, + pop: 38849, + }, + { + lat: 45.7086, + lng: 34.3933, + pop: 38714, + }, + { + lat: 32.5553, + lng: 51.5097, + pop: 37740, + }, + { + lat: 45.3667, + lng: 41.7167, + pop: 38100, + }, + { + lat: 34.6894, + lng: 40.8308, + pop: 37935, + }, + { + lat: 43.2944, + lng: 45.8839, + pop: 37373, + }, + { + lat: 36, + lng: 36.6667, + pop: 37490, + }, + { + lat: 34.2283, + lng: 37.2406, + pop: 37820, + }, + { + lat: 25.0742, + lng: 56.3553, + pop: 37545, + }, + { + lat: 44.6333, + lng: 39.1333, + pop: 37475, + }, + { + lat: 43.5, + lng: 44.75, + pop: 37442, + }, + { + lat: 37.6942, + lng: 37.8614, + pop: 37323, + }, + { + lat: 35.0653, + lng: 36.3422, + pop: 37109, + }, + { + lat: 43.2167, + lng: 46.8667, + pop: 37171, + }, + { + lat: 34.5814, + lng: -117.4397, + pop: 37229, + }, + { + lat: 43.1833, + lng: 44.55, + pop: 37029, + }, + { + lat: 43.8172, + lng: 28.5828, + pop: 36364, + }, + { + lat: 36.5083, + lng: 36.8692, + pop: 36562, + }, + { + lat: 41.1764, + lng: 29.6128, + pop: 36516, + }, + { + lat: 33.0167, + lng: 35.2708, + pop: 36000, + }, + { + lat: 29.1267, + lng: 54.0422, + pop: 36410, + }, + { + lat: 32.6368, + lng: 35.99, + pop: 34948, + }, + { + lat: 46.8, + lng: 33.4667, + pop: 35900, + }, + { + lat: 36.1569, + lng: 37.7078, + pop: 35409, + }, + { + lat: 34.6683, + lng: 36.2597, + pop: 35445, + }, + { + lat: 32.9658, + lng: 35.4983, + pop: 35700, + }, + { + lat: 32.4583, + lng: 35.8583, + pop: 35085, + }, + { + lat: 45.35, + lng: 42.85, + pop: 35745, + }, + { + lat: 41.4833, + lng: 31.8333, + pop: 35323, + }, + { + lat: 39.6381, + lng: 34.4672, + pop: 35561, + }, + { + lat: 35.9025, + lng: 36.0606, + pop: 35460, + }, + { + lat: 33.5333, + lng: 36.2167, + pop: 33571, + }, + { + lat: 35.2609, + lng: 36.3822, + pop: 35000, + }, + { + lat: 22.9339, + lng: 57.775, + pop: 35173, + }, + { + lat: 32.8536, + lng: 35.1978, + pop: 34000, + }, + { + lat: 40.95, + lng: 39.9333, + pop: 34831, + }, + { + lat: 44.3211, + lng: 28.6133, + pop: 34398, + }, + { + lat: 40.95, + lng: 38.7333, + pop: 34592, + }, + { + lat: 44.4167, + lng: 43.9167, + pop: 34690, + }, + { + lat: 29.2331, + lng: 56.6022, + pop: 34517, + }, + { + lat: 27.6672, + lng: 54.1411, + pop: 34469, + }, + { + lat: 35.4293, + lng: -119.0306, + pop: 34350, + }, + { + lat: 45.3594, + lng: 40.7072, + pop: 34215, + }, + { + lat: 40.6422, + lng: 29.1203, + pop: 34076, + }, + { + lat: 37.6834, + lng: -113.0956, + pop: 34246, + }, + { + lat: 31.5775, + lng: 54.4369, + pop: 34237, + }, + { + lat: 36.4889, + lng: 36.1944, + pop: 33540, + }, + { + lat: 41.0656, + lng: 37.7714, + pop: 33253, + }, + { + lat: 37.5764, + lng: 36.3506, + pop: 33193, + }, + { + lat: 32.7356, + lng: 36.0669, + pop: 32236, + }, + { + lat: 37.4247, + lng: 37.6928, + pop: 32846, + }, + { + lat: 34.8167, + lng: 36.1167, + pop: 32213, + }, + { + lat: 36.965, + lng: 37.5092, + pop: 32653, + }, + { + lat: 29.7742, + lng: 52.7236, + pop: 32261, + }, + { + lat: 26.2667, + lng: 50.15, + pop: 32067, + }, + { + lat: 46.05, + lng: 38.1667, + pop: 32180, + }, + { + lat: 35.217, + lng: -114.0105, + pop: 32204, + }, + { + lat: 32.9667, + lng: 36.0667, + pop: 31683, + }, + { + lat: 46.63, + lng: 31.1, + pop: 32100, + }, + { + lat: 41.0056, + lng: 38.8167, + pop: 32008, + }, + { + lat: 32.8667, + lng: 35.3, + pop: 31100, + }, + { + lat: 36.5866, + lng: 37.0463, + pop: 31534, + }, + { + lat: 32.2714, + lng: 50.9775, + pop: 31739, + }, + { + lat: 29.205, + lng: 52.69, + pop: 31711, + }, + { + lat: 39.8642, + lng: 36.5983, + pop: 31748, + }, + { + lat: 33.9667, + lng: 36.0167, + pop: 30000, + }, + { + lat: 33, + lng: 36.1167, + pop: 31258, + }, + { + lat: 23.5889, + lng: 58.4083, + pop: 31409, + }, + { + lat: 27.8236, + lng: 52.3303, + pop: 31436, + }, + { + lat: 39.8503, + lng: 33.4536, + pop: 31308, + }, + { + lat: 36.4731, + lng: 41.6161, + pop: 31161, + }, + { + lat: 47.2514, + lng: 35.7058, + pop: 31016, + }, + { + lat: 43.55, + lng: 43.85, + pop: 30832, + }, + { + lat: 32.5767, + lng: 51.455, + pop: 30002, + }, + { + lat: 32.2553, + lng: 50.5711, + pop: 30504, + }, + { + lat: 45.1, + lng: 43.45, + pop: 30530, + }, + { + lat: 33.7436, + lng: 36.7012, + pop: 30450, + }, + { + lat: 33.8667, + lng: 35.5667, + pop: 30000, + }, + { + lat: 44.415, + lng: 27.8236, + pop: 30217, + }, + { + lat: 42.2257, + lng: 43.9701, + pop: 30432, + }, + { + lat: 26.5581, + lng: 54.8806, + pop: 30435, + }, + { + lat: 44.0872, + lng: 41.9733, + pop: 30369, + }, + { + lat: 35.6139, + lng: 36.5611, + pop: 30000, + }, + { + lat: 32.5025, + lng: 35.6922, + pop: 29590, + }, + { + lat: 40.75, + lng: 36.3167, + pop: 30123, + }, + { + lat: 27.76, + lng: 54.0072, + pop: 29987, + }, + { + lat: 44.6672, + lng: 34.3978, + pop: 29668, + }, + { + lat: 27.3423, + lng: 53.1768, + pop: 29380, + }, + { + lat: 36.8503, + lng: 40.0706, + pop: 29347, + }, + { + lat: 35.2897, + lng: 36.7433, + pop: 29100, + }, + { + lat: 23.6522, + lng: 53.6536, + pop: 29095, + }, + { + lat: 30.2364, + lng: 49.7119, + pop: 29015, + }, + { + lat: 36.1242, + lng: -115.3324, + pop: 28861, + }, + { + lat: 37.486, + lng: 37.297, + pop: 28582, + }, + { + lat: 47.2667, + lng: 29.1667, + pop: 28500, + }, + { + lat: 40.3748, + lng: 36.9031, + pop: 28413, + }, + { + lat: 32.6417, + lng: 35.9417, + pop: 27902, + }, + { + lat: 33.0381, + lng: 40.2844, + pop: 28400, + }, + { + lat: 40.6975, + lng: 29.5114, + pop: 28232, + }, + { + lat: 41.0475, + lng: 39.2798, + pop: 28209, + }, + { + lat: 23.3833, + lng: 57.8167, + pop: 28088, + }, + { + lat: 40.1836, + lng: 31.3506, + pop: 28091, + }, + { + lat: 41.1158, + lng: 45.4853, + pop: 28000, + }, + { + lat: 35.6308, + lng: -117.6622, + pop: 27989, + }, + { + lat: 37.1303, + lng: -113.4878, + pop: 27689, + }, + { + lat: 41.8111, + lng: 41.7753, + pop: 27546, + }, + { + lat: 31.8889, + lng: 35.1675, + pop: 26604, + }, + { + lat: 31.1267, + lng: 53.2592, + pop: 27524, + }, + { + lat: 40.9142, + lng: 40.1125, + pop: 27428, + }, + { + lat: 35.5938, + lng: -119.3671, + pop: 27505, + }, + { + lat: 44.7167, + lng: 43, + pop: 27471, + }, + { + lat: 36.4733, + lng: 37.0972, + pop: 27086, + }, + { + lat: 44.7528, + lng: 33.8608, + pop: 27351, + }, + { + lat: 37.5375, + lng: 40.8892, + pop: 27304, + }, + { + lat: 43.65, + lng: 44.0667, + pop: 27074, + }, + { + lat: 29.8519, + lng: 51.5869, + pop: 26918, + }, + { + lat: 30.6131, + lng: 53.1953, + pop: 26933, + }, + { + lat: 33.7389, + lng: 36.6, + pop: 26671, + }, + { + lat: 36.1, + lng: 32.9667, + pop: 26840, + }, + { + lat: 40.8761, + lng: 37.7406, + pop: 26737, + }, + { + lat: 45.5, + lng: 41.2333, + pop: 26761, + }, + { + lat: 46.6742, + lng: 28.0597, + pop: 26266, + }, + { + lat: 33.725, + lng: 36.0972, + pop: 26285, + }, + { + lat: 40.8167, + lng: 39.6167, + pop: 26626, + }, + { + lat: 33.0711, + lng: 36.1842, + pop: 26268, + }, + { + lat: 32.6714, + lng: 35.2406, + pop: 25600, + }, + { + lat: 26.5833, + lng: 49.9833, + pop: 25500, + }, + { + lat: 36.699, + lng: -119.5575, + pop: 26424, + }, + { + lat: 41.4944, + lng: 36.0789, + pop: 26337, + }, + { + lat: 43.2044, + lng: 46.0911, + pop: 25672, + }, + { + lat: 37.1886, + lng: 32.2456, + pop: 26287, + }, + { + lat: 31.4142, + lng: 51.5694, + pop: 26260, + }, + { + lat: 41.9975, + lng: 43.5986, + pop: 26135, + }, + { + lat: 43.95, + lng: 43.6333, + pop: 26106, + }, + { + lat: 39.645, + lng: 41.5083, + pop: 25969, + }, + { + lat: 40.9697, + lng: 27.9553, + pop: 25873, + }, + { + lat: 36.2472, + lng: 29.9828, + pop: 25893, + }, + { + lat: 41.61, + lng: 35.595, + pop: 25854, + }, + { + lat: 45.9675, + lng: 33.8003, + pop: 25769, + }, + { + lat: 36.1167, + lng: 36.1333, + pop: 25118, + }, + { + lat: 30.0542, + lng: 50.1639, + pop: 25730, + }, + { + lat: 32.7711, + lng: 35.0394, + pop: 23700, + }, + { + lat: 32.61, + lng: 35.6081, + pop: 25000, + }, + { + lat: 41.0547, + lng: 30.8503, + pop: 25497, + }, + { + lat: 41.3636, + lng: 41.6792, + pop: 25500, + }, + { + lat: 44.1394, + lng: 43.0169, + pop: 24919, + }, + { + lat: 42.1625, + lng: 42.3417, + pop: 25318, + }, + { + lat: 40.1658, + lng: 38.0942, + pop: 25404, + }, + { + lat: 33.6967, + lng: 36.3739, + pop: 25194, + }, + { + lat: 44.6706, + lng: 41.838, + pop: 25279, + }, + { + lat: 40.7928, + lng: 42.6086, + pop: 25187, + }, + { + lat: 36.5988, + lng: -119.4471, + pop: 25168, + }, + { + lat: 29.9275, + lng: 56.5722, + pop: 25152, + }, + { + lat: 34.8661, + lng: -117.0471, + pop: 25123, + }, + { + lat: 45.1336, + lng: 33.5772, + pop: 24282, + }, + { + lat: 30.8947, + lng: 49.4092, + pop: 25009, + }, + { + lat: 34.2919, + lng: 35.9546, + pop: 25000, + }, + { + lat: 32.4097, + lng: 35.2808, + pop: 24439, + }, + { + lat: 40.6303, + lng: 48.6414, + pop: 24681, + }, + { + lat: 46.6333, + lng: 32.5833, + pop: 24639, + }, + { + lat: 36.5625, + lng: 35.3803, + pop: 24559, + }, + { + lat: 46.85, + lng: 40.3167, + pop: 24561, + }, + { + lat: 40.0494, + lng: 43.6608, + pop: 24560, + }, + { + lat: 36.5715, + lng: -119.6143, + pop: 24545, + }, + { + lat: 40.555, + lng: 44.9536, + pop: 23200, + }, + { + lat: 44.7506, + lng: 44.9797, + pop: 24472, + }, + { + lat: 32.8022, + lng: 51.6211, + pop: 24433, + }, + { + lat: 35.2661, + lng: 36.7114, + pop: 24105, + }, + { + lat: 36.5453, + lng: -119.3987, + pop: 24383, + }, + { + lat: 32.2514, + lng: 48.8161, + pop: 24216, + }, + { + lat: 36.1333, + lng: 36.45, + pop: 23700, + }, + { + lat: 27.8417, + lng: 51.9394, + pop: 24083, + }, + { + lat: 32.5542, + lng: 51.525, + pop: 23203, + }, + { + lat: 34.2511, + lng: 36.0111, + pop: 24000, + }, + { + lat: 41.0096, + lng: 44.3841, + pop: 23782, + }, + { + lat: 41.3903, + lng: 41.4194, + pop: 23846, + }, + { + lat: 33.7075, + lng: 35.9039, + pop: 23000, + }, + { + lat: 41.95, + lng: 34.5833, + pop: 23720, + }, + { + lat: 44.1167, + lng: 42.9833, + pop: 22891, + }, + { + lat: 34.6931, + lng: 32.9992, + pop: 22369, + }, + { + lat: 39.9211, + lng: 40.6947, + pop: 23589, + }, + { + lat: 45.7167, + lng: 42.9, + pop: 23579, + }, + { + lat: 40.8006, + lng: 32.1986, + pop: 23547, + }, + { + lat: 32.7667, + lng: 34.9667, + pop: 22200, + }, + { + lat: 23.55, + lng: 56.25, + pop: 23466, + }, + { + lat: 40.7931, + lng: 37.0164, + pop: 23369, + }, + { + lat: 43.1939, + lng: 45.2833, + pop: 23282, + }, + { + lat: 33.4333, + lng: 36.0833, + pop: 22831, + }, + { + lat: 41.8922, + lng: 33.0044, + pop: 23329, + }, + { + lat: 31.4086, + lng: 48.7942, + pop: 23211, + }, + { + lat: 33.2075, + lng: 35.5697, + pop: 23076, + }, + { + lat: 37.5467, + lng: 34.4844, + pop: 23252, + }, + { + lat: 40.4633, + lng: 42.7858, + pop: 23231, + }, + { + lat: 28.9306, + lng: 51.0689, + pop: 23178, + }, + { + lat: 32.0167, + lng: 35.7667, + pop: 21908, + }, + { + lat: 34.3914, + lng: 36.3958, + pop: 22250, + }, + { + lat: 40.6867, + lng: 37.3992, + pop: 22783, + }, + { + lat: 43.7731, + lng: 41.9169, + pop: 21067, + }, + { + lat: 32.6267, + lng: 51.4392, + pop: 22693, + }, + { + lat: 29.0664, + lng: 58.4047, + pop: 22761, + }, + { + lat: 41.0494, + lng: 39.2353, + pop: 22630, + }, + { + lat: 31.2656, + lng: 56.8056, + pop: 22729, + }, + { + lat: 39.6104, + lng: -119.777, + pop: 22622, + }, + { + lat: 36.0841, + lng: -119.5613, + pop: 22616, + }, + { + lat: 44.4256, + lng: 39.5319, + pop: 22468, + }, + { + lat: 40.0633, + lng: 44.4408, + pop: 21376, + }, + { + lat: 35.95, + lng: 36.7, + pop: 21848, + }, + { + lat: 41.5861, + lng: 32.6406, + pop: 22333, + }, + { + lat: 36.2, + lng: 36.5167, + pop: 21934, + }, + { + lat: 39.5627, + lng: -119.1906, + pop: 22343, + }, + { + lat: 37.2, + lng: 36.5833, + pop: 22242, + }, + { + lat: 40.3756, + lng: 43.4125, + pop: 22282, + }, + { + lat: 31.485, + lng: 48.2686, + pop: 22057, + }, + { + lat: 37.3192, + lng: 37.5686, + pop: 22192, + }, + { + lat: 40.8667, + lng: 35.2167, + pop: 22179, + }, + { + lat: 33.6, + lng: 36.515, + pop: 20559, + }, + { + lat: 32.6594, + lng: 35.11, + pop: 21383, + }, + { + lat: 41.0036, + lng: 36.6319, + pop: 21847, + }, + { + lat: 39.895, + lng: 37.7531, + pop: 21753, + }, + { + lat: 41.3081, + lng: 32.1417, + pop: 21655, + }, + { + lat: 41.0736, + lng: 36.0403, + pop: 21692, + }, + { + lat: 30.4775, + lng: 54.2128, + pop: 21690, + }, + { + lat: 41.2, + lng: 32.3292, + pop: 21625, + }, + { + lat: 42.2689, + lng: 42.0678, + pop: 21596, + }, + { + lat: 36.3756, + lng: 36.9942, + pop: 21039, + }, + { + lat: 33.2539, + lng: 35.2717, + pop: 20000, + }, + { + lat: 32.2269, + lng: 50.7931, + pop: 21352, + }, + { + lat: 39.8728, + lng: 44.5192, + pop: 21311, + }, + { + lat: 36.5667, + lng: 36.1333, + pop: 20459, + }, + { + lat: 40.35, + lng: 30.0167, + pop: 20976, + }, + { + lat: 34.3722, + lng: 41.9875, + pop: 21000, + }, + { + lat: 31.8711, + lng: 35.4442, + pop: 20300, + }, + { + lat: 39.8303, + lng: 44.7025, + pop: 20800, + }, + { + lat: 42.1083, + lng: 43.0417, + pop: 20814, + }, + { + lat: 43.65, + lng: 43.55, + pop: 20718, + }, + { + lat: 28.6689, + lng: 59.0733, + pop: 20720, + }, + { + lat: 37.4278, + lng: 34.8711, + pop: 20683, + }, + { + lat: 41.3333, + lng: 41.3, + pop: 20565, + }, + { + lat: 34.8658, + lng: -118.2155, + pop: 20574, + }, + { + lat: 43.4, + lng: 42.9167, + pop: 20513, + }, + { + lat: 39.6333, + lng: 43.3778, + pop: 20450, + }, + { + lat: 40.7333, + lng: 38.4333, + pop: 20405, + }, + { + lat: 40.9, + lng: 31.05, + pop: 20266, + }, + { + lat: 32.7781, + lng: 51.6461, + pop: 20301, + }, + { + lat: 43.2419, + lng: 46, + pop: 20013, + }, + { + lat: 28.8714, + lng: 52.0917, + pop: 20320, + }, + { + lat: 41.4969, + lng: 44.8108, + pop: 20211, + }, + { + lat: 32.555, + lng: 51.5731, + pop: 19406, + }, + { + lat: 37.0669, + lng: 36.1464, + pop: 20127, + }, + { + lat: 34.2028, + lng: 35.6544, + pop: 20000, + }, + { + lat: 40.1894, + lng: 39.1267, + pop: 20084, + }, + { + lat: 40.6172, + lng: 43.9758, + pop: 19543, + }, + { + lat: 46.7333, + lng: 29.7, + pop: 20000, + }, + { + lat: 43.2242, + lng: 46.1942, + pop: 19727, + }, + { + lat: 43.0333, + lng: 44.2333, + pop: 20043, + }, + { + lat: 36.8032, + lng: -114.133, + pop: 20019, + }, + { + lat: 35.4794, + lng: -119.2013, + pop: 19897, + }, + { + lat: 30.895, + lng: 50.0931, + pop: 19857, + }, + { + lat: 32.5167, + lng: 36.4833, + pop: 19683, + }, + { + lat: 40.0731, + lng: 35.4947, + pop: 19786, + }, + { + lat: 41.4167, + lng: 35.05, + pop: 19650, + }, + { + lat: 34.4686, + lng: 41.9167, + pop: 19629, + }, + { + lat: 35.1944, + lng: -118.8306, + pop: 19568, + }, + { + lat: 43.4833, + lng: 44.1333, + pop: 19494, + }, + { + lat: 34.9478, + lng: 33.5881, + pop: 19199, + }, + { + lat: 40.8106, + lng: 41.5269, + pop: 19510, + }, + { + lat: 37.1487, + lng: -113.3517, + pop: 19501, + }, + { + lat: 43.1833, + lng: 44.3167, + pop: 19412, + }, + { + lat: 31.4619, + lng: 48.0739, + pop: 19481, + }, + { + lat: 36.7678, + lng: 35.7922, + pop: 18587, + }, + { + lat: 32.8454, + lng: 36.2499, + pop: 19158, + }, + { + lat: 27.8914, + lng: 53.4344, + pop: 19347, + }, + { + lat: 46.1667, + lng: 34.8, + pop: 19253, + }, + { + lat: 40.0186, + lng: 30.1814, + pop: 19244, + }, + { + lat: 27.5014, + lng: 52.5858, + pop: 18837, + }, + { + lat: 26.5992, + lng: 54.9361, + pop: 19213, + }, + { + lat: 32.0006, + lng: 54.2075, + pop: 19123, + }, + { + lat: 46.7, + lng: 41.7333, + pop: 19032, + }, + { + lat: 29.3606, + lng: 51.0683, + pop: 18913, + }, + { + lat: 28.9864, + lng: 51.0375, + pop: 18702, + }, + { + lat: 31.9436, + lng: 34.8392, + pop: 18401, + }, + { + lat: 35.3886, + lng: -119.2058, + pop: 18875, + }, + { + lat: 41.2422, + lng: 33.3283, + pop: 18863, + }, + { + lat: 31.9833, + lng: 35.7667, + pop: 17754, + }, + { + lat: 32.5, + lng: 34.9167, + pop: 17759, + }, + { + lat: 45.45, + lng: 29.2667, + pop: 18745, + }, + { + lat: 43.1642, + lng: 45.6228, + pop: 18534, + }, + { + lat: 40.8139, + lng: 32.8908, + pop: 18694, + }, + { + lat: 39.6731, + lng: 33.6136, + pop: 18139, + }, + { + lat: 34.4398, + lng: -117.5248, + pop: 18599, + }, + { + lat: 36.65, + lng: 36.2167, + pop: 17925, + }, + { + lat: 31.9408, + lng: 54.2736, + pop: 18309, + }, + { + lat: 31.9414, + lng: 54.2828, + pop: 18309, + }, + { + lat: 29.5, + lng: 53.3167, + pop: 18477, + }, + { + lat: 47.3907, + lng: 35.0027, + pop: 18468, + }, + { + lat: 31.7492, + lng: 54.2103, + pop: 18464, + }, + { + lat: 47.0333, + lng: 28.95, + pop: 17210, + }, + { + lat: 43.2081, + lng: 44.8186, + pop: 17734, + }, + { + lat: 32.5, + lng: 35.5, + pop: 18200, + }, + { + lat: 31.5419, + lng: 60.0364, + pop: 18304, + }, + { + lat: 40.5619, + lng: 42.3464, + pop: 18281, + }, + { + lat: 29.2736, + lng: 53.2203, + pop: 18187, + }, + { + lat: 33.6797, + lng: 35.5583, + pop: 17000, + }, + { + lat: 28.4758, + lng: 57.8481, + pop: 18185, + }, + { + lat: 43.1667, + lng: 46, + pop: 17970, + }, + { + lat: 33.6422, + lng: 36.2978, + pop: 17521, + }, + { + lat: 32.3836, + lng: 51.5147, + pop: 17966, + }, + { + lat: 35.25, + lng: 36.5833, + pop: 17578, + }, + { + lat: 36.1231, + lng: 37.3369, + pop: 17767, + }, + { + lat: 36.2583, + lng: 41.9431, + pop: 18000, + }, + { + lat: 34.4818, + lng: -118.6316, + pop: 18017, + }, + { + lat: 35.1, + lng: 33.3667, + pop: 16774, + }, + { + lat: 45.4575, + lng: 28.2711, + pop: 17736, + }, + { + lat: 26.2258, + lng: 60.2142, + pop: 17732, + }, + { + lat: 41.0314, + lng: 36.2683, + pop: 17628, + }, + { + lat: 34.9203, + lng: 40.5594, + pop: 17537, + }, + { + lat: 29.9125, + lng: 53.3086, + pop: 17642, + }, + { + lat: 35.3697, + lng: 36.38, + pop: 17313, + }, + { + lat: 41.2433, + lng: 42.3639, + pop: 17606, + }, + { + lat: 32.3781, + lng: 51.3181, + pop: 17335, + }, + { + lat: 44.1778, + lng: 43.5, + pop: 17451, + }, + { + lat: 46.1167, + lng: 32.9167, + pop: 17344, + }, + { + lat: 45.6855, + lng: 28.6134, + pop: 17400, + }, + { + lat: 35.3736, + lng: 36.6017, + pop: 17052, + }, + { + lat: 40.8483, + lng: 43.3317, + pop: 17373, + }, + { + lat: 30.0775, + lng: 53.1331, + pop: 17131, + }, + { + lat: 46.9753, + lng: 28.8194, + pop: 15934, + }, + { + lat: 40.07, + lng: 47.2047, + pop: 16998, + }, + { + lat: 40.2981, + lng: 41.6325, + pop: 17054, + }, + { + lat: 40.9833, + lng: 39.8, + pop: 15503, + }, + { + lat: 44.3381, + lng: 28.0336, + pop: 17022, + }, + { + lat: 41.1333, + lng: 41.0167, + pop: 16902, + }, + { + lat: 31.94, + lng: 51.6478, + pop: 16899, + }, + { + lat: 32.8542, + lng: 36.6292, + pop: 16745, + }, + { + lat: 40.7408, + lng: 44.8631, + pop: 16600, + }, + { + lat: 25.6439, + lng: 57.7744, + pop: 16860, + }, + { + lat: 46.0833, + lng: 40.8583, + pop: 16838, + }, + { + lat: 36.4575, + lng: 41.7061, + pop: 16798, + }, + { + lat: 44.8514, + lng: 34.9725, + pop: 16784, + }, + { + lat: 40.9, + lng: 38.4167, + pop: 16758, + }, + { + lat: 46.05, + lng: 28.8333, + pop: 16605, + }, + { + lat: 32.4725, + lng: 35.7928, + pop: 16000, + }, + { + lat: 32.7333, + lng: 36.2, + pop: 16240, + }, + { + lat: 37.5465, + lng: 35.3987, + pop: 16653, + }, + { + lat: 35.3208, + lng: 36.6225, + pop: 16267, + }, + { + lat: 41.0833, + lng: 39.3833, + pop: 16335, + }, + { + lat: 43.9333, + lng: 42.5167, + pop: 16512, + }, + { + lat: 40.646, + lng: 34.261, + pop: 16525, + }, + { + lat: 45.0544, + lng: 34.6022, + pop: 16428, + }, + { + lat: 32.8353, + lng: 35.9714, + pop: 15985, + }, + { + lat: 36.2333, + lng: 36.2167, + pop: 15692, + }, + { + lat: 32.3936, + lng: 51.3408, + pop: 16086, + }, + { + lat: 40.7475, + lng: 40.2419, + pop: 16213, + }, + { + lat: 39.9756, + lng: 41.8711, + pop: 16178, + }, + { + lat: 43.6003, + lng: 46.7789, + pop: 16165, + }, + { + lat: 34.8868, + lng: 38.8721, + pop: 16173, + }, + { + lat: 39.6568, + lng: -119.6694, + pop: 16131, + }, + { + lat: 40.4425, + lng: 47.6767, + pop: 16018, + }, + { + lat: 33.2692, + lng: 35.7706, + pop: 15973, + }, + { + lat: 41.9486, + lng: 34.3367, + pop: 16004, + }, + { + lat: 46.7006, + lng: 32.5478, + pop: 15984, + }, + { + lat: 27.7975, + lng: 53.685, + pop: 16000, + }, + { + lat: 46.1167, + lng: 48.0833, + pop: 15984, + }, + { + lat: 33.1047, + lng: 50.9589, + pop: 15828, + }, + { + lat: 37.0164, + lng: 41.9544, + pop: 15759, + }, + { + lat: 35.0004, + lng: -114.5748, + pop: 15872, + }, + { + lat: 43.4333, + lng: 28.3333, + pop: 15834, + }, + { + lat: 36.0561, + lng: 40.7303, + pop: 15806, + }, + { + lat: 31.4469, + lng: 49.5294, + pop: 15802, + }, + { + lat: 32.7025, + lng: 51.1536, + pop: 15673, + }, + { + lat: 35.9632, + lng: 38.0356, + pop: 15477, + }, + { + lat: 33.0528, + lng: 51.0825, + pop: 15550, + }, + { + lat: 33.4242, + lng: 36.2244, + pop: 13993, + }, + { + lat: 40.52, + lng: 35.2953, + pop: 15655, + }, + { + lat: 27.4064, + lng: 57.5014, + pop: 15634, + }, + { + lat: 34.7087, + lng: 33.0504, + pop: 14578, + }, + { + lat: 40.9667, + lng: 31.45, + pop: 15573, + }, + { + lat: 32.8019, + lng: 51.6636, + pop: 15524, + }, + { + lat: 30.8733, + lng: 55.2706, + pop: 15532, + }, + { + lat: 40.0172, + lng: 32.3483, + pop: 15540, + }, + { + lat: 36.3667, + lng: 36.2, + pop: 14751, + }, + { + lat: 31.4825, + lng: 48.8747, + pop: 15312, + }, + { + lat: 47.2848, + lng: 39.4823, + pop: 15334, + }, + { + lat: 40.879, + lng: 37.4532, + pop: 14954, + }, + { + lat: 40.8372, + lng: 44.2675, + pop: 15000, + }, + { + lat: 35.0333, + lng: 33.9833, + pop: 14963, + }, + { + lat: 30.7461, + lng: 50.7461, + pop: 15218, + }, + { + lat: 28.8833, + lng: 51.275, + pop: 15198, + }, + { + lat: 39.8033, + lng: 29.6178, + pop: 15181, + }, + { + lat: 46.5167, + lng: 32.5167, + pop: 15163, + }, + { + lat: 41.45, + lng: 45.1, + pop: 15100, + }, + { + lat: 34.2597, + lng: 36.4236, + pop: 15000, + }, + { + lat: 33.6333, + lng: 35.7833, + pop: 14728, + }, + { + lat: 32.4917, + lng: 36.7111, + pop: 15000, + }, + { + lat: 39.2592, + lng: -119.5653, + pop: 15036, + }, + { + lat: 32.5379, + lng: 34.9122, + pop: 13962, + }, + { + lat: 37.544, + lng: 41.72, + pop: 14976, + }, + { + lat: 35.9033, + lng: 36.7258, + pop: 14530, + }, + { + lat: 40.8736, + lng: 30.9508, + pop: 14895, + }, + { + lat: 36.6975, + lng: 38.9567, + pop: 14825, + }, + { + lat: 35.1578, + lng: -117.8721, + pop: 14914, + }, + { + lat: 35.8407, + lng: -114.9257, + pop: 14868, + }, + { + lat: 41.7494, + lng: 32.3864, + pop: 14776, + }, + { + lat: 43.17, + lng: 45.3711, + pop: 14720, + }, + { + lat: 42.4167, + lng: 27.7, + pop: 14789, + }, + { + lat: 45.85, + lng: 41.5167, + pop: 14761, + }, + { + lat: 40.9333, + lng: 38.1333, + pop: 14659, + }, + { + lat: 45.3694, + lng: 44.2281, + pop: 14722, + }, + { + lat: 46.6186, + lng: 31.5392, + pop: 14705, + }, + { + lat: 30.2625, + lng: 51.9833, + pop: 14633, + }, + { + lat: 33.6497, + lng: 35.4433, + pop: 12888, + }, + { + lat: 40.8856, + lng: 39.2922, + pop: 14592, + }, + { + lat: 26.9636, + lng: 56.0622, + pop: 14525, + }, + { + lat: 35.3753, + lng: 36.6872, + pop: 14307, + }, + { + lat: 33.7421, + lng: 36.6435, + pop: 14228, + }, + { + lat: 37.4025, + lng: 40.9561, + pop: 14233, + }, + { + lat: 43.2162, + lng: 46.0381, + pop: 14111, + }, + { + lat: 40.262, + lng: 36.313, + pop: 14335, + }, + { + lat: 39.71, + lng: 39.7017, + pop: 14390, + }, + { + lat: 46.1269, + lng: 30.385, + pop: 14321, + }, + { + lat: 37.4431, + lng: 36.0322, + pop: 14308, + }, + { + lat: 32.4467, + lng: 35.1703, + pop: 13640, + }, + { + lat: 35.2659, + lng: -118.9159, + pop: 14269, + }, + { + lat: 39.6936, + lng: 35.5111, + pop: 14198, + }, + { + lat: 34.7181, + lng: 33.0856, + pop: 13421, + }, + { + lat: 43.2036, + lng: 46.1322, + pop: 13824, + }, + { + lat: 41.05, + lng: 39.1333, + pop: 13955, + }, + { + lat: 42.65, + lng: 27.7333, + pop: 14146, + }, + { + lat: 44.27, + lng: 28.56, + pop: 13968, + }, + { + lat: 40.9, + lng: 30.4833, + pop: 13973, + }, + { + lat: 35.6781, + lng: -119.2413, + pop: 14085, + }, + { + lat: 25.7347, + lng: 51.5475, + pop: 13511, + }, + { + lat: 43.1636, + lng: 45.4725, + pop: 13836, + }, + { + lat: 36.255, + lng: 42.0164, + pop: 14000, + }, + { + lat: 41.0333, + lng: 37.1, + pop: 13922, + }, + { + lat: 27.4744, + lng: 52.6114, + pop: 13557, + }, + { + lat: 39.9106, + lng: 44.7278, + pop: 13600, + }, + { + lat: 46.6791, + lng: 32.7228, + pop: 12812, + }, + { + lat: 28.4061, + lng: 54.1881, + pop: 13809, + }, + { + lat: 36.3947, + lng: 36.6889, + pop: 13661, + }, + { + lat: 36.2828, + lng: 36.8519, + pop: 13525, + }, + { + lat: 40.5572, + lng: 39.2919, + pop: 13771, + }, + { + lat: 43.25, + lng: 46.1333, + pop: 13405, + }, + { + lat: 32.4167, + lng: 35.6833, + pop: 13056, + }, + { + lat: 47.0833, + lng: 39.5667, + pop: 13692, + }, + { + lat: 36.5833, + lng: 31.8833, + pop: 13563, + }, + { + lat: 34.8969, + lng: 36.1346, + pop: 13244, + }, + { + lat: 27.2856, + lng: 61.9964, + pop: 13580, + }, + { + lat: 32.8425, + lng: 36.34, + pop: 13315, + }, + { + lat: 32.9411, + lng: 50.1211, + pop: 13475, + }, + { + lat: 41.925, + lng: 44.4222, + pop: 13423, + }, + { + lat: 30.8939, + lng: 61.6803, + pop: 13357, + }, + { + lat: 33.93, + lng: 35.745, + pop: 12000, + }, + { + lat: 41.4647, + lng: 47.74, + pop: 13405, + }, + { + lat: 32.0078, + lng: 51.2156, + pop: 13317, + }, + { + lat: 35.41, + lng: 36.39, + pop: 12925, + }, + { + lat: 34.7833, + lng: 36.4333, + pop: 13020, + }, + { + lat: 35.1276, + lng: -118.4744, + pop: 13346, + }, + { + lat: 41.6667, + lng: 48.1333, + pop: 13232, + }, + { + lat: 36.5133, + lng: 41.9542, + pop: 13281, + }, + { + lat: 41.3228, + lng: 47.1133, + pop: 13260, + }, + { + lat: 32.7109, + lng: 36.0266, + pop: 12640, + }, + { + lat: 31.19, + lng: 50.4419, + pop: 13269, + }, + { + lat: 29.2911, + lng: 56.9131, + pop: 13263, + }, + { + lat: 40.9814, + lng: 47.8458, + pop: 13190, + }, + { + lat: 41.1333, + lng: 44.65, + pop: 13000, + }, + { + lat: 40.16, + lng: 47.1722, + pop: 13002, + }, + { + lat: 27.5236, + lng: 57.8811, + pop: 13169, + }, + { + lat: 46.3425, + lng: 30.5653, + pop: 13036, + }, + { + lat: 37.4711, + lng: 41.9139, + pop: 13091, + }, + { + lat: 37.4792, + lng: 40.4864, + pop: 13117, + }, + { + lat: 44.8832, + lng: 39.1902, + pop: 12745, + }, + { + lat: 45.3908, + lng: 47.3658, + pop: 13125, + }, + { + lat: 43.5167, + lng: 43.7, + pop: 12813, + }, + { + lat: 32.6428, + lng: 51.5, + pop: 11264, + }, + { + lat: 25.6208, + lng: 51.0819, + pop: 13085, + }, + { + lat: 37.0519, + lng: 31.7842, + pop: 13084, + }, + { + lat: 26.5758, + lng: 59.6397, + pop: 13070, + }, + { + lat: 32.5444, + lng: 50.7461, + pop: 12971, + }, + { + lat: 30.0547, + lng: 54.3717, + pop: 13032, + }, + { + lat: 45.9072, + lng: 43.3558, + pop: 12998, + }, + { + lat: 35.0611, + lng: 36.6972, + pop: 12194, + }, + { + lat: 34.5596, + lng: -117.9558, + pop: 12961, + }, + { + lat: 34.3436, + lng: 36.4756, + pop: 12000, + }, + { + lat: 32.4197, + lng: 52.6483, + pop: 12714, + }, + { + lat: 43.1878, + lng: 44.9036, + pop: 12734, + }, + { + lat: 42.2903, + lng: 43.2819, + pop: 12803, + }, + { + lat: 35.2645, + lng: -114.0091, + pop: 12858, + }, + { + lat: 34.7594, + lng: -112.412, + pop: 12854, + }, + { + lat: 43.2906, + lng: 45.3014, + pop: 12738, + }, + { + lat: 35.6342, + lng: 36.6322, + pop: 12276, + }, + { + lat: 45.1142, + lng: 34.0142, + pop: 12711, + }, + { + lat: 30.9817, + lng: 50.4233, + pop: 12772, + }, + { + lat: 43.4847, + lng: 44.5881, + pop: 12614, + }, + { + lat: 43.4333, + lng: 43.575, + pop: 10829, + }, + { + lat: 40.1806, + lng: 45.72, + pop: 12363, + }, + { + lat: 40.3053, + lng: 37.8306, + pop: 12637, + }, + { + lat: 36.6964, + lng: 32.6203, + pop: 12601, + }, + { + lat: 40.9667, + lng: 39.7333, + pop: 11077, + }, + { + lat: 44.1128, + lng: 28.5558, + pop: 12333, + }, + { + lat: 34.1542, + lng: 36.7442, + pop: 12508, + }, + { + lat: 44.165, + lng: 28.455, + pop: 12376, + }, + { + lat: 43.3458, + lng: 44.2028, + pop: 12501, + }, + { + lat: 43.2586, + lng: 45.5392, + pop: 12340, + }, + { + lat: 36.2082, + lng: -119.0897, + pop: 12551, + }, + { + lat: 32.3378, + lng: 51.1961, + pop: 12292, + }, + { + lat: 34.6097, + lng: -117.8339, + pop: 12497, + }, + { + lat: 34.2419, + lng: 35.9794, + pop: 12000, + }, + { + lat: 43.4269, + lng: 28.1617, + pop: 12429, + }, + { + lat: 39.9011, + lng: 38.7686, + pop: 12456, + }, + { + lat: 26.5583, + lng: 49.9503, + pop: 11460, + }, + { + lat: 43.2514, + lng: 45.9072, + pop: 12224, + }, + { + lat: 36.5244, + lng: -119.5602, + pop: 12413, + }, + { + lat: 32.3156, + lng: 50.6783, + pop: 12308, + }, + { + lat: 36.2936, + lng: 37.0444, + pop: 11918, + }, + { + lat: 40.14, + lng: 45.3064, + pop: 11987, + }, + { + lat: 43.3117, + lng: 45.1594, + pop: 12221, + }, + { + lat: 32.2854, + lng: 35.8113, + pop: 11586, + }, + { + lat: 43.1126, + lng: 45.7339, + pop: 12092, + }, + { + lat: 40.3167, + lng: 38.7667, + pop: 12250, + }, + { + lat: 42.665, + lng: 46.22, + pop: 12159, + }, + { + lat: 45.6833, + lng: 28.4028, + pop: 12185, + }, + { + lat: 27.1944, + lng: 60.4558, + pop: 12217, + }, + { + lat: 46.5331, + lng: 48.3456, + pop: 12214, + }, + { + lat: 34.5683, + lng: 36.2764, + pop: 12000, + }, + { + lat: 46.8333, + lng: 33.4167, + pop: 12123, + }, + { + lat: 32.6653, + lng: 35.7333, + pop: 11706, + }, + { + lat: 43.6756, + lng: 43.455, + pop: 12001, + }, + { + lat: 30.0042, + lng: 53.0067, + pop: 12000, + }, + { + lat: 41.8833, + lng: 34.9167, + pop: 12049, + }, + { + lat: 47.0708, + lng: 32.7997, + pop: 12045, + }, + { + lat: 31.9383, + lng: 51.0533, + pop: 11980, + }, + { + lat: 33.1839, + lng: 36.2264, + pop: 11802, + }, + { + lat: 40.9131, + lng: 37.5169, + pop: 11851, + }, + { + lat: 36.9883, + lng: 32.4569, + pop: 11970, + }, + { + lat: 36.6667, + lng: 34.4167, + pop: 11923, + }, + { + lat: 47.2717, + lng: 35.2248, + pop: 11949, + }, + { + lat: 40.9422, + lng: 39.1942, + pop: 11934, + }, + { + lat: 31.9911, + lng: 54.2322, + pop: 11691, + }, + { + lat: 42.4333, + lng: 47.3167, + pop: 11862, + }, + { + lat: 47.1708, + lng: 37.6954, + pop: 10350, + }, + { + lat: 46.2667, + lng: 30.4333, + pop: 11741, + }, + { + lat: 47.1333, + lng: 28.8667, + pop: 10669, + }, + { + lat: 33.2092, + lng: 35.2992, + pop: 10000, + }, + { + lat: 33.5581, + lng: 36.2222, + pop: 10045, + }, + { + lat: 43.6119, + lng: 43.3269, + pop: 11717, + }, + { + lat: 36.8175, + lng: 38.0111, + pop: 11570, + }, + { + lat: 38.8957, + lng: -119.7492, + pop: 11761, + }, + { + lat: 46.7353, + lng: 36.3473, + pop: 11679, + }, + { + lat: 32.4689, + lng: 51.5578, + pop: 10851, + }, + { + lat: 43.5667, + lng: 43.5833, + pop: 11575, + }, + { + lat: 26.2483, + lng: 60.7525, + pop: 11605, + }, + { + lat: 43.4911, + lng: 43.5528, + pop: 9669, + }, + { + lat: 46.6977, + lng: 35.1554, + pop: 11481, + }, + { + lat: 36.8833, + lng: 36.2333, + pop: 11187, + }, + { + lat: 46.9122, + lng: 28.8839, + pop: 10175, + }, + { + lat: 42.3264, + lng: 42.6006, + pop: 11281, + }, + { + lat: 36.6389, + lng: 32.8925, + pop: 11332, + }, + { + lat: 29.9789, + lng: 48.5206, + pop: 11173, + }, + { + lat: 30.0617, + lng: 48.4508, + pop: 11173, + }, + { + lat: 39.7667, + lng: 30.95, + pop: 11242, + }, + { + lat: 44.45, + lng: 42.5, + pop: 11215, + }, + { + lat: 46.1083, + lng: 28.5972, + pop: 11123, + }, + { + lat: 36.1417, + lng: 33.3178, + pop: 11088, + }, + { + lat: 34.2425, + lng: 37.0589, + pop: 11064, + }, + { + lat: 46.6833, + lng: 47.85, + pop: 11079, + }, + { + lat: 36.7667, + lng: 31.3889, + pop: 11000, + }, + { + lat: 47.0667, + lng: 28.6833, + pop: 10380, + }, + { + lat: 34.0833, + lng: 36.7667, + pop: 10984, + }, + { + lat: 44.4361, + lng: 34.1106, + pop: 10310, + }, + { + lat: 46.8678, + lng: 28.7689, + pop: 10907, + }, + { + lat: 45.5019, + lng: 32.7025, + pop: 11039, + }, + { + lat: 43.5261, + lng: 43.5594, + pop: 11004, + }, + { + lat: 36.1389, + lng: 36.83, + pop: 10657, + }, + { + lat: 41.6281, + lng: 48.6828, + pop: 10894, + }, + { + lat: 40.7444, + lng: 43.625, + pop: 10985, + }, + { + lat: 36.2167, + lng: 36.1667, + pop: 10354, + }, + { + lat: 33.4861, + lng: 36.6011, + pop: 10548, + }, + { + lat: 33.7333, + lng: 35.45, + pop: 10000, + }, + { + lat: 32.6872, + lng: 34.9383, + pop: 10639, + }, + { + lat: 37.3406, + lng: 40.8258, + pop: 10846, + }, + { + lat: 41.3, + lng: 27.95, + pop: 10601, + }, + { + lat: 44.5528, + lng: 34.2875, + pop: 9117, + }, + { + lat: 36.6667, + lng: 34.3833, + pop: 10907, + }, + { + lat: 32.2983, + lng: 48.4289, + pop: 10858, + }, + { + lat: 40.1428, + lng: 44.1164, + pop: 9870, + }, + { + lat: 41.3833, + lng: 27.9333, + pop: 10072, + }, + { + lat: 41.7333, + lng: 45.3333, + pop: 10871, + }, + { + lat: 40.9368, + lng: 45.8258, + pop: 10797, + }, + { + lat: 34.3, + lng: 35.8, + pop: 10000, + }, + { + lat: 36.7167, + lng: 36.2333, + pop: 10574, + }, + { + lat: 40.3217, + lng: 44.4814, + pop: 10656, + }, + { + lat: 35.683, + lng: 36.533, + pop: 10353, + }, + { + lat: 36.2333, + lng: 36.8167, + pop: 10394, + }, + { + lat: 32.6872, + lng: 36.3508, + pop: 10510, + }, + { + lat: 45.4978, + lng: 34.295, + pop: 10766, + }, + { + lat: 35.0211, + lng: 33.42, + pop: 10466, + }, + { + lat: 41.4783, + lng: 46.6175, + pop: 10700, + }, + { + lat: 30.3611, + lng: 51.1572, + pop: 10764, + }, + { + lat: 36.8667, + lng: 36.2, + pop: 10482, + }, + { + lat: 44.6667, + lng: 45.65, + pop: 10641, + }, + { + lat: 33.85, + lng: 35.6667, + pop: 10000, + }, + { + lat: 32.6322, + lng: 36.3386, + pop: 10466, + }, + { + lat: 31.4103, + lng: 56.2825, + pop: 10761, + }, + { + lat: 45.0448, + lng: 42.1104, + pop: 10695, + }, + { + lat: 31.6325, + lng: 49.8897, + pop: 10698, + }, + { + lat: 44.7444, + lng: 44.2031, + pop: 10721, + }, + { + lat: 29.2417, + lng: 57.3253, + pop: 10670, + }, + { + lat: 37.4944, + lng: 30.9817, + pop: 10707, + }, + { + lat: 40.3833, + lng: 35.5167, + pop: 10703, + }, + { + lat: 44.6142, + lng: 33.6083, + pop: 10196, + }, + { + lat: 46.3544, + lng: 34.3361, + pop: 10647, + }, + { + lat: 41.9747, + lng: 33.7608, + pop: 10594, + }, + { + lat: 33.3986, + lng: 36.4531, + pop: 10473, + }, + { + lat: 43.0878, + lng: 46.5631, + pop: 10532, + }, + { + lat: 40.2847, + lng: 30.3172, + pop: 10591, + }, + { + lat: 46.1958, + lng: 41.0778, + pop: 10593, + }, + { + lat: 36.0833, + lng: 36.5, + pop: 10296, + }, + { + lat: 29.5636, + lng: 51.3369, + pop: 10508, + }, + { + lat: 41.8389, + lng: 43.3792, + pop: 10546, + }, + { + lat: 40, + lng: 29.9, + pop: 10527, + }, + { + lat: 46.6167, + lng: 29.9167, + pop: 10436, + }, + { + lat: 32.3772, + lng: 51.1883, + pop: 10279, + }, + { + lat: 40.78, + lng: 43.1353, + pop: 10497, + }, + { + lat: 40.0986, + lng: 44.4681, + pop: 9550, + }, + { + lat: 36.834, + lng: 37.999, + pop: 10436, + }, + { + lat: 23.4675, + lng: 58.1061, + pop: 10396, + }, + { + lat: 36.305, + lng: -119.2083, + pop: 10441, + }, + { + lat: 30.1811, + lng: 56.8019, + pop: 10407, + }, + { + lat: 40.1167, + lng: 35.2667, + pop: 10407, + }, + { + lat: 46.7559, + lng: 33.4247, + pop: 10360, + }, + { + lat: 36.2941, + lng: -119.1459, + pop: 10349, + }, + { + lat: 44.1736, + lng: 28.4083, + pop: 10216, + }, + { + lat: 25.7089, + lng: 55.7972, + pop: 10190, + }, + { + lat: 36.1164, + lng: 36.5147, + pop: 10084, + }, + { + lat: 40.8333, + lng: 33.25, + pop: 10307, + }, + { + lat: 29.5978, + lng: 57.4386, + pop: 10286, + }, + { + lat: 27.4753, + lng: 59.4717, + pop: 10292, + }, + { + lat: 32.6828, + lng: 36.2233, + pop: 9784, + }, + { + lat: 40.9419, + lng: 45.7358, + pop: 10130, + }, + { + lat: 46.5036, + lng: 30.3244, + pop: 10148, + }, + { + lat: 39.7981, + lng: 42.6744, + pop: 10191, + }, + { + lat: 45.6333, + lng: 27.8, + pop: 10126, + }, + { + lat: 28.8678, + lng: 52.7533, + pop: 10120, + }, + { + lat: 46.9139, + lng: 28.9708, + pop: 9966, + }, + { + lat: 31.9989, + lng: 50.6617, + pop: 10113, + }, + { + lat: 26.2361, + lng: 61.3986, + pop: 10115, + }, + { + lat: 37.471, + lng: 42.317, + pop: 10094, + }, + { + lat: 43.1581, + lng: 44.1569, + pop: 10075, + }, + { + lat: 43.071, + lng: 46.6345, + pop: 10014, + }, + { + lat: 34.6527, + lng: -118.2163, + pop: 10079, + }, + { + lat: 46.0903, + lng: 47.7306, + pop: 10036, + }, + { + lat: 40.8808, + lng: 45.3917, + pop: 9864, + }, + { + lat: 39.75, + lng: 28.9167, + pop: 10042, + }, + { + lat: 35.1409, + lng: -118.4968, + pop: 10051, + }, + { + lat: 27.6594, + lng: 52.6575, + pop: 9982, + }, + { + lat: 43.2317, + lng: 45.5722, + pop: 9783, + }, + { + lat: 40.1331, + lng: 45.4367, + pop: 9880, + }, + { + lat: 36.9667, + lng: 35.05, + pop: 8689, + }, + { + lat: 27.1992, + lng: 54.3667, + pop: 9959, + }, + { + lat: 31.9336, + lng: 51.3306, + pop: 9923, + }, + { + lat: 33.0736, + lng: 50.1647, + pop: 9933, + }, + { + lat: 35.6169, + lng: 36.5953, + pop: 9595, + }, + { + lat: 43.1328, + lng: 45.7797, + pop: 9738, + }, + { + lat: 32.7039, + lng: 51.8381, + pop: 9712, + }, + { + lat: 47.2039, + lng: 30.9125, + pop: 9845, + }, + { + lat: 41.1289, + lng: 43.1328, + pop: 9833, + }, + { + lat: 42.3503, + lng: 42.9983, + pop: 9770, + }, + { + lat: 28.745, + lng: 53.8033, + pop: 9719, + }, + { + lat: 40.1333, + lng: 38.7333, + pop: 9759, + }, + { + lat: 47.0189, + lng: 34.9212, + pop: 9719, + }, + { + lat: 32.8658, + lng: 51.5972, + pop: 9690, + }, + { + lat: 36.8708, + lng: 39.025, + pop: 9653, + }, + { + lat: 40.7814, + lng: 43.8964, + pop: 9668, + }, + { + lat: 43.1797, + lng: 45.4081, + pop: 9584, + }, + { + lat: 45.1278, + lng: 39.5725, + pop: 9617, + }, + { + lat: 40.5194, + lng: 28.8281, + pop: 9625, + }, + { + lat: 44.6833, + lng: 27.9519, + pop: 9642, + }, + { + lat: 46.3667, + lng: 28.5167, + pop: 9562, + }, + { + lat: 46.6317, + lng: 32.4452, + pop: 9565, + }, + { + lat: 33.6878, + lng: 36.1008, + pop: 9371, + }, + { + lat: 44.8228, + lng: 44.6592, + pop: 9602, + }, + { + lat: 39.7175, + lng: 44.8764, + pop: 9306, + }, + { + lat: 46.3629, + lng: 33.5302, + pop: 9539, + }, + { + lat: 43.3725, + lng: 46.445, + pop: 9442, + }, + { + lat: 43.2, + lng: 45.7889, + pop: 9300, + }, + { + lat: 45.4464, + lng: 34.7344, + pop: 9460, + }, + { + lat: 43.9625, + lng: 42.9875, + pop: 9427, + }, + { + lat: 47.1503, + lng: 29.2925, + pop: 9381, + }, + { + lat: 40.21, + lng: 39.6511, + pop: 9387, + }, + { + lat: 29.0147, + lng: 61.45, + pop: 9359, + }, + { + lat: 28.0842, + lng: 54.0483, + pop: 9318, + }, + { + lat: 44.5167, + lng: 34.1833, + pop: 8571, + }, + { + lat: 47.0875, + lng: 28.8703, + pop: 8694, + }, + { + lat: 44.8981, + lng: 28.7419, + pop: 9213, + }, + { + lat: 46.2581, + lng: 33.2843, + pop: 9224, + }, + { + lat: 31.8719, + lng: 56.0239, + pop: 9232, + }, + { + lat: 29.885, + lng: 57.7306, + pop: 9205, + }, + { + lat: 47.25, + lng: 28.7667, + pop: 9122, + }, + { + lat: 40, + lng: 36.22, + pop: 9154, + }, + { + lat: 45.85, + lng: 28.6944, + pop: 9138, + }, + { + lat: 27.8722, + lng: 52.0289, + pop: 8753, + }, + { + lat: 35.5464, + lng: 36.6431, + pop: 8817, + }, + { + lat: 29.6, + lng: 55.5369, + pop: 9112, + }, + { + lat: 43.3186, + lng: 45.9878, + pop: 8972, + }, + { + lat: 35.2477, + lng: -116.6834, + pop: 9100, + }, + { + lat: 41.6222, + lng: 35.5314, + pop: 8864, + }, + { + lat: 46.7833, + lng: 29.6167, + pop: 9000, + }, + { + lat: 35.0025, + lng: 40.5117, + pop: 9000, + }, + { + lat: 39.4737, + lng: -118.7779, + pop: 9068, + }, + { + lat: 40.1, + lng: 31.6833, + pop: 9039, + }, + { + lat: 39.8056, + lng: 40.0364, + pop: 9032, + }, + { + lat: 40.75, + lng: 33.7667, + pop: 8981, + }, + { + lat: 42.3244, + lng: 42.4222, + pop: 8987, + }, + { + lat: 41.45, + lng: 44.5333, + pop: 8967, + }, + { + lat: 41.5, + lng: 31.8667, + pop: 8678, + }, + { + lat: 32.3828, + lng: 35.6619, + pop: 8647, + }, + { + lat: 44.2333, + lng: 42.0167, + pop: 8836, + }, + { + lat: 28.3106, + lng: 54.3347, + pop: 8927, + }, + { + lat: 40.1467, + lng: 45.2642, + pop: 8553, + }, + { + lat: 35.9969, + lng: 36.7867, + pop: 8540, + }, + { + lat: 46.1353, + lng: 41.9656, + pop: 8798, + }, + { + lat: 29.4769, + lng: 54.3314, + pop: 8799, + }, + { + lat: 37.1742, + lng: -113.6809, + pop: 8786, + }, + { + lat: 41.1244, + lng: 44.2819, + pop: 8700, + }, + { + lat: 40.283, + lng: 35.267, + pop: 8696, + }, + { + lat: 35.1268, + lng: -119.4243, + pop: 8730, + }, + { + lat: 32.5756, + lng: 59.7983, + pop: 8715, + }, + { + lat: 36.5433, + lng: -119.2914, + pop: 8701, + }, + { + lat: 37.1703, + lng: 34.6083, + pop: 8679, + }, + { + lat: 42.5658, + lng: 47.5631, + pop: 8627, + }, + { + lat: 40.1639, + lng: 39.8925, + pop: 8657, + }, + { + lat: 37.0728, + lng: 40.6519, + pop: 8551, + }, + { + lat: 44.7494, + lng: 43.4386, + pop: 8544, + }, + { + lat: 37.6494, + lng: 30.5339, + pop: 8537, + }, + { + lat: 40.1575, + lng: 33.7175, + pop: 8531, + }, + { + lat: 32.2631, + lng: 51.5622, + pop: 9924, + }, + { + lat: 32.0686, + lng: 61.8058, + pop: 10000, + }, + { + lat: 33.7711, + lng: 35.6858, + pop: 10000, + }, + { + lat: 30.4667, + lng: 53.45, + pop: 9776, + }, + { + lat: 36.5833, + lng: 31.8833, + pop: 9527, + }, + { + lat: 43.2167, + lng: 46.8667, + pop: 9458, + }, + { + lat: 40.9447, + lng: 47.9411, + pop: 9507, + }, + { + lat: 44.2659, + lng: 43.7562, + pop: 9516, + }, + { + lat: 30.4344, + lng: 63.3183, + pop: 9482, + }, + { + lat: 43.0997, + lng: 44.6317, + pop: 9217, + }, + { + lat: 32.1447, + lng: 48.3925, + pop: 9177, + }, + { + lat: 44.0817, + lng: 42.9606, + pop: 9079, + }, + { + lat: 43.42, + lng: 43.92, + pop: 9010, + }, + { + lat: 40.5808, + lng: 46.8503, + pop: 8830, + }, + { + lat: 32.2539, + lng: 50.5975, + pop: 8699, + }, + { + lat: 40.9533, + lng: 45.6792, + pop: 8702, + }, + { + lat: 43.3469, + lng: 44.6975, + pop: 8590, + }, + { + lat: 46.5467, + lng: 30.6306, + pop: 8558, + }, + { + lat: 43.1486, + lng: 44.7069, + pop: 8508, + }, + { + lat: 40.9053, + lng: 45.5564, + pop: 1155, + }, +] \ No newline at end of file diff --git a/frontend/react/src/unit/databases/groundunitdatabase.ts b/frontend/react/src/unit/databases/groundunitdatabase.ts new file mode 100644 index 00000000..778cc262 --- /dev/null +++ b/frontend/react/src/unit/databases/groundunitdatabase.ts @@ -0,0 +1,36 @@ +import { getApp } from "../../olympusapp"; +import { GAME_MASTER } from "../../constants/constants"; +import { UnitDatabase } from "./unitdatabase" + +export class GroundUnitDatabase extends UnitDatabase { + constructor() { + super('api/databases/units/groundunitdatabase'); + } + + getSpawnPointsByName(name: string) { + if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns) + return 0; + + const blueprint = this.getByName(name); + if (blueprint?.cost != undefined) + return blueprint?.cost; + + if (blueprint?.era == "WW2") + return 20; + else if (blueprint?.era == "Early Cold War") + return 50; + else if (blueprint?.era == "Mid Cold War") + return 100; + else if (blueprint?.era == "Late Cold War") + return 200; + else if (blueprint?.era == "Modern") + return 400; + return 0; + } + + getCategory() { + return "GroundUnit"; + } +} + +export var groundUnitDatabase = new GroundUnitDatabase(); diff --git a/frontend/react/src/unit/databases/helicopterdatabase.ts b/frontend/react/src/unit/databases/helicopterdatabase.ts new file mode 100644 index 00000000..23ca4e5c --- /dev/null +++ b/frontend/react/src/unit/databases/helicopterdatabase.ts @@ -0,0 +1,37 @@ +import { getApp } from "../../olympusapp"; +import { GAME_MASTER } from "../../constants/constants"; +import { UnitDatabase } from "./unitdatabase" + +export class HelicopterDatabase extends UnitDatabase { + constructor() { + super('api/databases/units/helicopterdatabase'); + } + + getSpawnPointsByName(name: string) { + if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns) + return 0; + + const blueprint = this.getByName(name); + if (blueprint?.cost != undefined) + return blueprint?.cost; + + if (blueprint?.era == "WW2") + return 20; + else if (blueprint?.era == "Early Cold War") + return 50; + else if (blueprint?.era == "Mid Cold War") + return 100; + else if (blueprint?.era == "Late Cold War") + return 200; + else if (blueprint?.era == "Modern") + return 400; + return 0; + } + + getCategory() { + return "Helicopter"; + } +} + +export var helicopterDatabase = new HelicopterDatabase(); + diff --git a/frontend/react/src/unit/databases/navyunitdatabase.ts b/frontend/react/src/unit/databases/navyunitdatabase.ts new file mode 100644 index 00000000..3de8f35e --- /dev/null +++ b/frontend/react/src/unit/databases/navyunitdatabase.ts @@ -0,0 +1,36 @@ +import { getApp } from "../../olympusapp"; +import { GAME_MASTER } from "../../constants/constants"; +import { UnitDatabase } from "./unitdatabase" + +export class NavyUnitDatabase extends UnitDatabase { + constructor() { + super('api/databases/units/navyunitdatabase'); + } + + getSpawnPointsByName(name: string) { + if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns) + return 0; + + const blueprint = this.getByName(name); + if (blueprint?.cost != undefined) + return blueprint?.cost; + + if (blueprint?.era == "WW2") + return 20; + else if (blueprint?.era == "Early Cold War") + return 50; + else if (blueprint?.era == "Mid Cold War") + return 100; + else if (blueprint?.era == "Late Cold War") + return 200; + else if (blueprint?.era == "Modern") + return 400; + return 0; + } + + getCategory() { + return "NavyUnit"; + } +} + +export var navyUnitDatabase = new NavyUnitDatabase(); diff --git a/frontend/react/src/unit/databases/unitdatabase.ts b/frontend/react/src/unit/databases/unitdatabase.ts new file mode 100644 index 00000000..6737817b --- /dev/null +++ b/frontend/react/src/unit/databases/unitdatabase.ts @@ -0,0 +1,239 @@ +import { LatLng } from "leaflet"; +import { getApp } from "../../olympusapp"; +import { GAME_MASTER } from "../../constants/constants"; +import { UnitBlueprint } from "../../interfaces"; + +export abstract class UnitDatabase { + blueprints: { [key: string]: UnitBlueprint } = {}; + #url: string; + + constructor(url: string = "") { + this.#url = url; + this.load(() => {}); + } + + load(callback: CallableFunction) { + if (this.#url !== "") { + var xhr = new XMLHttpRequest(); + xhr.open('GET', this.#url, true); + xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0"); + xhr.responseType = 'json'; + xhr.onload = () => { + var status = xhr.status; + if (status === 200) { + this.blueprints = xhr.response; + callback(); + } else { + console.error(`Error retrieving database from ${this.#url}`) + } + }; + xhr.send(); + } + } + + abstract getCategory(): string; + + /* Gets a specific blueprint by name */ + getByName(name: string) { + if (name in this.blueprints) + return this.blueprints[name]; + return null; + } + + /* Gets a specific blueprint by label */ + getByLabel(label: string) { + for (let unit in this.blueprints) { + if (this.blueprints[unit].label === label) + return this.blueprints[unit]; + } + return null; + } + + getBlueprints(includeDisabled: boolean = false) { + if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns) { + var filteredBlueprints: { [key: string]: UnitBlueprint } = {}; + for (let unit in this.blueprints) { + const blueprint = this.blueprints[unit]; + if (blueprint.enabled || includeDisabled) + filteredBlueprints[unit] = blueprint; + } + return filteredBlueprints; + } + else { + var filteredBlueprints: { [key: string]: UnitBlueprint } = {}; + for (let unit in this.blueprints) { + const blueprint = this.blueprints[unit]; + if ((blueprint.enabled || includeDisabled) && this.getSpawnPointsByName(blueprint.name) <= getApp().getMissionManager().getAvailableSpawnPoints() && + getApp().getMissionManager().getCommandModeOptions().eras.includes(blueprint.era) && + (!getApp().getMissionManager().getCommandModeOptions().restrictToCoalition || blueprint.coalition === getApp().getMissionManager().getCommandedCoalition() || blueprint.coalition === undefined)) { + filteredBlueprints[unit] = blueprint; + } + } + return filteredBlueprints; + } + } + + /* Returns a list of all possible roles in a database */ + getRoles() { + var roles: string[] = []; + var filteredBlueprints = this.getBlueprints(); + for (let unit in filteredBlueprints) { + var loadouts = filteredBlueprints[unit].loadouts; + if (loadouts) { + for (let loadout of loadouts) { + for (let role of loadout.roles) { + if (role !== "" && !roles.includes(role)) + roles.push(role); + } + } + } + } + return roles; + } + + /* Returns a list of all possible types in a database */ + getTypes(unitFilter?:CallableFunction) { + var filteredBlueprints = this.getBlueprints(); + var types: string[] = []; + for (let unit in filteredBlueprints) { + if ( typeof unitFilter === "function" && !unitFilter(filteredBlueprints[unit])) + continue; + var type = filteredBlueprints[unit].type; + if (type && type !== "" && !types.includes(type)) + types.push(type); + } + return types; + } + + /* Returns a list of all possible periods in a database */ + getEras() { + var filteredBlueprints = this.getBlueprints(); + var eras: string[] = []; + for (let unit in filteredBlueprints) { + var era = filteredBlueprints[unit].era; + if (era && era !== "" && !eras.includes(era)) + eras.push(era); + } + return eras; + } + + /* Get all blueprints by range */ + getByRange(range: string) { + var filteredBlueprints = this.getBlueprints(); + var unitswithrange = []; + var minRange = 0; + var maxRange = 0; + + if (range === "Short range") { + minRange = 0; + maxRange = 10000; + } + else if (range === "Medium range") { + minRange = 10000; + maxRange = 100000; + } + else { + minRange = 100000; + maxRange = 999999; + } + + for (let unit in filteredBlueprints) { + var engagementRange = filteredBlueprints[unit].engagementRange; + if (engagementRange !== undefined) { + if (engagementRange >= minRange && engagementRange < maxRange) { + unitswithrange.push(filteredBlueprints[unit]); + } + } + } + return unitswithrange; + } + + /* Get all blueprints by type */ + getByType(type: string) { + var filteredBlueprints = this.getBlueprints(); + var units = []; + for (let unit in filteredBlueprints) { + if (filteredBlueprints[unit].type === type) { + units.push(filteredBlueprints[unit]); + } + } + return units; + } + + /* Get all blueprints by role */ + getByRole(role: string) { + var filteredBlueprints = this.getBlueprints(); + var units = []; + for (let unit in filteredBlueprints) { + var loadouts = filteredBlueprints[unit].loadouts; + if (loadouts) { + for (let loadout of loadouts) { + if (loadout.roles.includes(role) || loadout.roles.includes(role.toLowerCase())) { + units.push(filteredBlueprints[unit]) + break; + } + } + } + } + return units; + } + + /* Get the names of all the loadouts for a specific unit and for a specific role */ + getLoadoutNamesByRole(name: string, role: string) { + var filteredBlueprints = this.getBlueprints(); + var loadoutsByRole = []; + var loadouts = filteredBlueprints[name].loadouts; + if (loadouts) { + for (let loadout of loadouts) { + if (loadout.roles.includes(role) || loadout.roles.includes("")) { + loadoutsByRole.push(loadout.name) + } + } + } + return loadoutsByRole; + } + + /* Get the livery names for a specific unit */ + getLiveryNamesByName(name: string) { + var liveries = this.blueprints[name].liveries; + if (liveries !== undefined) + return Object.values(liveries); + else + return []; + } + + /* Get the loadout content from the unit name and loadout name */ + getLoadoutByName(name: string, loadoutName: string) { + var loadouts = this.blueprints[name].loadouts; + if (loadouts) { + for (let loadout of loadouts) { + if (loadout.name === loadoutName) + return loadout; + } + } + return null; + } + + getSpawnPointsByLabel(label: string) { + var blueprint = this.getByLabel(label); + if (blueprint) + return this.getSpawnPointsByName(blueprint.name); + else + return Infinity; + } + + getSpawnPointsByName(name: string) { + return Infinity; + } + + getUnkownUnit(name: string): UnitBlueprint { + return { + name: name, + enabled: true, + coalition: 'neutral', + era: 'N/A', + label: name, + shortLabel: '' + } + } +} \ No newline at end of file diff --git a/frontend/react/src/unit/group.ts b/frontend/react/src/unit/group.ts new file mode 100644 index 00000000..38d703a3 --- /dev/null +++ b/frontend/react/src/unit/group.ts @@ -0,0 +1,45 @@ +import { Unit } from "./unit"; + +export class Group { + #members: Unit[] = []; + #name: string; + + constructor(name: string) { + this.#name = name; + + document.addEventListener("unitDeath", (e: any) => { + if (this.#members.includes(e.detail)) + this.getLeader()?.onGroupChanged(e.detail); + }); + } + + getName() { + return this.#name; + } + + addMember(member: Unit) { + if (!this.#members.includes(member)) { + this.#members.push(member); + member.setGroup(this); + + this.getLeader()?.onGroupChanged(member); + } + } + + removeMember(member: Unit) { + if (this.#members.includes(member)) { + delete this.#members[this.#members.indexOf(member)]; + member.setGroup(null); + + this.getLeader()?.onGroupChanged(member); + } + } + + getMembers() { + return this.#members; + } + + getLeader() { + return this.#members.find((unit: Unit) => { return (unit.getIsLeader() && unit.getAlive())}) + } +} \ No newline at end of file diff --git a/frontend/react/src/unit/importexport/unitdatafile.ts b/frontend/react/src/unit/importexport/unitdatafile.ts new file mode 100644 index 00000000..e7e845ae --- /dev/null +++ b/frontend/react/src/unit/importexport/unitdatafile.ts @@ -0,0 +1,66 @@ +//import { Dialog } from "../../dialog/dialog"; +//import { createCheckboxOption } from "../../other/utils"; + +var categoryMap = { + "Aircraft": "Aircraft", + "Helicopter": "Helicopter", + "GroundUnit": "Ground units", + "NavyUnit": "Naval units" +} + +export abstract class UnitDataFile { + + protected data: any; + //protected dialog!: Dialog; + + constructor() { } + + buildCategoryCoalitionTable() { + + const categories = this.#getCategoriesFromData(); + const coalitions = ["blue", "neutral", "red"]; + + let headersHTML: string = ``; + let matrixHTML: string = ``; + + //categories.forEach((category: string, index) => { + // matrixHTML += `${categoryMap[category as keyof typeof categoryMap]}`; +// + // coalitions.forEach((coalition: string) => { + // if (index === 0) + // headersHTML += `${coalition[0].toUpperCase() + coalition.substring(1)}`; +// + // const optionIsValid = this.data[category].hasOwnProperty(coalition); + // let checkboxHTML = createCheckboxOption(``, category, optionIsValid, () => { }, { + // "disabled": !optionIsValid, + // "name": "category-coalition-selection", + // "readOnly": !optionIsValid, + // "value" : `${category}:${coalition}` + // }).outerHTML; +// + // if (optionIsValid) + // checkboxHTML = checkboxHTML.replace(`"checkbox"`, `"checkbox" checked`); // inner and outerHTML screw default checked up +// + // matrixHTML += `${checkboxHTML}`; +// + // }); + // matrixHTML += ""; + //}); +// + //const table = this.dialog.getElement().querySelector("table.categories-coalitions"); + + //(table.tHead).innerHTML = ` ${headersHTML}`; + //(table.querySelector(`tbody`)).innerHTML = matrixHTML; + + } + + #getCategoriesFromData() { + const categories = Object.keys(this.data); + categories.sort(); + return categories; + } + + getData() { + return this.data; + } +} \ No newline at end of file diff --git a/frontend/react/src/unit/importexport/unitdatafileexport.ts b/frontend/react/src/unit/importexport/unitdatafileexport.ts new file mode 100644 index 00000000..c145f9ca --- /dev/null +++ b/frontend/react/src/unit/importexport/unitdatafileexport.ts @@ -0,0 +1,101 @@ +import { getApp } from "../../olympusapp"; +//import { Dialog } from "../../dialog/dialog"; +import { zeroAppend } from "../../other/utils"; +import { Unit } from "../unit"; +import { UnitDataFile } from "./unitdatafile"; + +export class UnitDataFileExport extends UnitDataFile { + + protected data!: any; + //protected dialog: Dialog; + #element!: HTMLElement; + #filename: string = "export.json"; + + constructor(elementId: string) { + super(); + //this.dialog = new Dialog(elementId); + //this.#element = this.dialog.getElement(); + + this.#element.querySelector(".start-transfer")?.addEventListener("click", (ev: MouseEventInit) => { + this.#doExport(); + }); + } + + /** + * Show the form to start the export journey + */ + showForm(units: Unit[]) { + //this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => { + // el.classList.toggle("hide", el.getAttribute("data-on-error") === "show"); + //}); + // + //const data: any = {}; + //const unitCanBeExported = (unit: Unit) => !["Aircraft", "Helicopter"].includes(unit.getCategory()); +// + //units.filter((unit: Unit) => unit.getAlive() && unitCanBeExported(unit)).forEach((unit: Unit) => { + // const category = unit.getCategory(); + // const coalition = unit.getCoalition(); +// + // if (!data.hasOwnProperty(category)) { + // data[category] = {}; + // } +// + // if (!data[category].hasOwnProperty(coalition)) + // data[category][coalition] = []; +// + // data[category][coalition].push(unit); + //}); +// + //this.data = data; + //this.buildCategoryCoalitionTable(); + //this.dialog.show(); +// + //const date = new Date(); + //this.#filename = `olympus_${getApp().getMissionManager().getTheatre().replace(/[^\w]/gi, "").toLowerCase()}_${date.getFullYear()}${zeroAppend(date.getMonth() + 1, 2)}${zeroAppend(date.getDate(), 2)}_${zeroAppend(date.getHours(), 2)}${zeroAppend(date.getMinutes(), 2)}${zeroAppend(date.getSeconds(), 2)}.json`; + //var input = this.#element.querySelector("#export-filename") as HTMLInputElement; + //input.onchange = (ev: Event) => { + // this.#filename = (ev.currentTarget as HTMLInputElement).value; + //} + //if (input) + // input.value = this.#filename; + } + + #doExport() { + + let selectedUnits: Unit[] = []; + + this.#element.querySelectorAll(`input[type="checkbox"][name="category-coalition-selection"]:checked`).forEach((checkbox: HTMLInputElement) => { + if (checkbox instanceof HTMLInputElement) { + const [category, coalition] = checkbox.value.split(":"); // e.g. "category:coalition" + selectedUnits = selectedUnits.concat(this.data[category][coalition]); + } + }); + + if (selectedUnits.length === 0) { + alert("Please select at least one option for export."); + return; + } + + var unitsToExport: { [key: string]: any } = {}; + selectedUnits.forEach((unit: Unit) => { + var data: any = unit.getData(); + if (unit.getGroupName() in unitsToExport) + unitsToExport[unit.getGroupName()].push(data); + else + unitsToExport[unit.getGroupName()] = [data]; + }); + + + const a = document.createElement("a"); + const file = new Blob([JSON.stringify(unitsToExport)], { type: 'text/plain' }); + a.href = URL.createObjectURL(file); + + var filename = this.#filename; + if (!this.#filename.toLowerCase().endsWith(".json")) + filename += ".json"; + a.download = filename; + a.click(); + //this.dialog.hide(); + } + +} \ No newline at end of file diff --git a/frontend/react/src/unit/importexport/unitdatafileimport.ts b/frontend/react/src/unit/importexport/unitdatafileimport.ts new file mode 100644 index 00000000..33c62c49 --- /dev/null +++ b/frontend/react/src/unit/importexport/unitdatafileimport.ts @@ -0,0 +1,150 @@ +import { getApp } from "../../olympusapp"; +//import { Dialog } from "../../dialog/dialog"; +import { UnitData } from "../../interfaces"; +//import { ImportFileJSONSchemaValidator } from "../../schemas/schema"; +import { UnitDataFile } from "./unitdatafile"; + +export class UnitDataFileImport extends UnitDataFile { + + protected data!: any; + //protected dialog: Dialog; + #fileData!: { [key: string]: UnitData[] }; + + constructor(elementId: string) { + super(); + //this.dialog = new Dialog(elementId); + //this.dialog.getElement().querySelector(".start-transfer")?.addEventListener("click", (ev: MouseEventInit) => { + // this.#doImport(); + // this.dialog.hide(); + //}); + } + + #doImport() { + + //let selectedCategories: any = {}; + //const unitsManager = getApp().getUnitsManager(); +// + //this.dialog.getElement().querySelectorAll(`input[type="checkbox"][name="category-coalition-selection"]:checked`).forEach((checkbox: HTMLInputElement) => { + // if (checkbox instanceof HTMLInputElement) { + // const [category, coalition] = checkbox.value.split(":"); // e.g. "category:coalition" + // selectedCategories[category] = selectedCategories[category] || {}; + // selectedCategories[category][coalition] = true; + // } + //}); +// + //for (const [groupName, groupData] of Object.entries(this.#fileData)) { + // if (groupName === "" || groupData.length === 0 || !this.#unitGroupDataCanBeImported(groupData)) + // continue; +// + // let { category, coalition } = groupData[0]; +// + // if (!selectedCategories.hasOwnProperty(category) + // || !selectedCategories[category].hasOwnProperty(coalition) + // || selectedCategories[category][coalition] !== true) + // continue; +// + // let unitsToSpawn = groupData.filter((unitData: UnitData) => this.#unitDataCanBeImported(unitData)).map((unitData: UnitData) => { + // return { unitType: unitData.name, location: unitData.position, liveryID: "", skill: "High" } + // }); +// + // unitsManager.spawnUnits(category, unitsToSpawn, coalition, false); + //} + } + + selectFile() { + var input = document.createElement("input"); + input.type = "file"; + input.addEventListener("change", (e: any) => { + var file = e.target.files[0]; + if (!file) { + return; + } + var reader = new FileReader(); + reader.onload = (e: any) => { + + try { + this.#fileData = JSON.parse(e.target.result); + + //const validator = new ImportFileJSONSchemaValidator(); + //if (!validator.validate(this.#fileData)) { + // const errors = validator.getErrors().reduce((acc:any, error:any) => { + // let errorString = error.instancePath.substring(1) + ": " + error.message; + // if (error.params) { + // const {allowedValues} = error.params; + // if (allowedValues) + // errorString += ": " + allowedValues.join(', '); + // } + // acc.push(errorString); + // return acc; + // }, [] as string[]); + // this.#showFileDataErrors(errors); + //} else { + // this.#showForm(); + //} + } catch(e:any) { + this.#showFileDataErrors([e]); + } + }; + reader.readAsText(file); + }) + input.click(); + } + + #showFileDataErrors( reasons:string[]) { + + //this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => { + // el.classList.toggle("hide", el.getAttribute("data-on-error") === "hide"); + //}); +// + //const reasonsList = this.dialog.getElement().querySelector(".import-error-reasons"); + //if (reasonsList instanceof HTMLElement) + // reasonsList.innerHTML = `
  • ${reasons.join("
  • ")}
  • `; +// + //this.dialog.show(); + } + + #showForm() { + //this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => { + // el.classList.toggle("hide", el.getAttribute("data-on-error") === "show"); + //}); +// + //const data: any = {}; +// + //for (const [group, units] of Object.entries(this.#fileData)) { + // if (group === "" || units.length === 0) + // continue; +// + // if (units.some((unit: UnitData) => !this.#unitDataCanBeImported(unit))) + // continue; +// + // const category = units[0].category; +// + // if (!data.hasOwnProperty(category)) { + // data[category] = {}; + // } +// + // units.forEach((unit: UnitData) => { + // if (!data[category].hasOwnProperty(unit.coalition)) + // data[category][unit.coalition] = []; +// + // data[category][unit.coalition].push(unit); + // }); +// + //} +// + //this.data = data; + //this.buildCategoryCoalitionTable(); + //this.dialog.show(); + } + + #unitDataCanBeImported(unitData: UnitData) { + return unitData.alive && this.#unitGroupDataCanBeImported([unitData]); + } + + #unitGroupDataCanBeImported(unitGroupData: UnitData[]) { + return unitGroupData.every((unitData: UnitData) => { + return !["Aircraft", "Helicopter"].includes(unitData.category); + }) && unitGroupData.some((unitData: UnitData) => unitData.alive); + } + +} \ No newline at end of file diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts new file mode 100644 index 00000000..d025b913 --- /dev/null +++ b/frontend/react/src/unit/unit.ts @@ -0,0 +1,1729 @@ +import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point } from 'leaflet'; +import { getApp } from '../olympusapp'; +import { enumToCoalition, enumToEmissioNCountermeasure, enumToROE, enumToReactionToThreat, enumToState, getUnitDatabaseByCategory, mToFt, msToKnots, rad2deg, bearing, deg2rad, ftToM, getGroundElevation, coalitionToEnum, nmToFt, nmToM, zeroAppend } from '../other/utils'; +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 { DataExtractor } from '../server/dataextractor'; +import { groundUnitDatabase } from './databases/groundunitdatabase'; +import { navyUnitDatabase } from './databases/navyunitdatabase'; +import { Weapon } from '../weapon/weapon'; +import { Ammo, Contact, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitData } from '../interfaces'; +import { RangeCircle } from "../map/rangecircle"; +import { Group } from './group'; +import { ContextActionSet } from './contextactionset'; +import * as turf from "@turf/turf"; + +var pathIcon = new Icon({ + iconUrl: '/resources/theme/images/markers/marker-icon.png', + shadowUrl: '/resources/theme/images/markers/marker-shadow.png', + iconAnchor: [13, 41] +}); + +/** + * Unit class which controls unit behaviour + */ +export abstract class Unit extends CustomMarker { + ID: number; + + /* Data controlled directly by the backend. No setters are provided to avoid misalignments */ + #alive: boolean = false; + #human: boolean = false; + #controlled: boolean = false; + #coalition: string = "neutral"; + #country: number = 0; + #name: string = ""; + #unitName: string = ""; + #groupName: string = ""; + #state: string = states[0]; + #task: string = "" + #hasTask: boolean = false; + #position: LatLng = new LatLng(0, 0, 0); + #speed: number = 0; + #horizontalVelocity: number = 0; + #verticalVelocity: number = 0; + #heading: number = 0; + #track: number = 0; + #isActiveTanker: boolean = false; + #isActiveAWACS: boolean = false; + #onOff: boolean = true; + #followRoads: boolean = false; + #fuel: number = 0; + #desiredSpeed: number = 0; + #desiredSpeedType: string = "CAS"; + #desiredAltitude: number = 0; + #desiredAltitudeType: string = "ASL"; + #leaderID: number = 0; + #formationOffset: Offset = { + x: 0, + y: 0, + z: 0 + }; + #targetID: number = 0; + #targetPosition: LatLng = new LatLng(0, 0); + #ROE: string = ROEs[1]; + #reactionToThreat: string = reactionsToThreat[2]; + #emissionsCountermeasures: string = emissionsCountermeasures[2]; + #TACAN: TACAN = { + isOn: false, + XY: 'X', + callsign: 'TKR', + channel: 0 + }; + #radio: Radio = { + frequency: 124000000, + callsign: 1, + callsignNumber: 1 + }; + #generalSettings: GeneralSettings = { + prohibitAA: false, + prohibitAfterburner: false, + prohibitAG: false, + prohibitAirWpn: false, + prohibitJettison: false + }; + #ammo: Ammo[] = []; + #contacts: Contact[] = []; + #activePath: LatLng[] = []; + #isLeader: boolean = false; + #operateAs: string = "blue"; + #shotsScatter: number = 2; + #shotsIntensity: number = 2; + #health: number = 100; + + /* Other members used to draw the unit, mostly ancillary stuff like targets, ranges and so on */ + #group: Group | null = null; + #selected: boolean = false; + #hidden: boolean = false; + #highlighted: boolean = false; + #waitingForDoubleClick: boolean = false; + #pathMarkers: Marker[] = []; + #pathPolyline: Polyline; + #contactsPolylines: Polyline[] = []; + #engagementCircle: RangeCircle; + #acquisitionCircle: RangeCircle; + #miniMapMarker: CircleMarker | null = null; + #targetPositionMarker: TargetMarker; + #targetPositionPolyline: Polyline; + #doubleClickTimer: number = 0; + #hotgroup: number | null = null; + #detectionMethods: number[] = []; + + /* Getters for backend driven data */ + getAlive() { return this.#alive }; + getHuman() { return this.#human }; + getControlled() { return this.#controlled }; + getCoalition() { return this.#coalition }; + getCountry() { return this.#country }; + getName() { return this.#name }; + getUnitName() { return this.#unitName }; + getGroupName() { return this.#groupName }; + getState() { return this.#state }; + getTask() { return this.#task }; + getHasTask() { return this.#hasTask }; + getPosition() { return this.#position }; + getSpeed() { return this.#speed }; + getHorizontalVelocity() { return this.#horizontalVelocity }; + getVerticalVelocity() { return this.#verticalVelocity }; + getHeading() { return this.#heading }; + getTrack() { return this.#track }; + getIsActiveAWACS() { return this.#isActiveAWACS }; + getIsActiveTanker() { return this.#isActiveTanker }; + getOnOff() { return this.#onOff }; + getFollowRoads() { return this.#followRoads }; + getFuel() { return this.#fuel }; + getDesiredSpeed() { return this.#desiredSpeed }; + getDesiredSpeedType() { return this.#desiredSpeedType }; + getDesiredAltitude() { return this.#desiredAltitude }; + getDesiredAltitudeType() { return this.#desiredAltitudeType }; + getLeaderID() { return this.#leaderID }; + getFormationOffset() { return this.#formationOffset }; + getTargetID() { return this.#targetID }; + getTargetPosition() { return this.#targetPosition }; + getROE() { return this.#ROE }; + getReactionToThreat() { return this.#reactionToThreat }; + getEmissionsCountermeasures() { return this.#emissionsCountermeasures }; + getTACAN() { return this.#TACAN }; + getRadio() { return this.#radio }; + getGeneralSettings() { return this.#generalSettings }; + getAmmo() { return this.#ammo }; + getContacts() { return this.#contacts }; + getActivePath() { return this.#activePath }; + getIsLeader() { return this.#isLeader }; + getOperateAs() { return this.#operateAs }; + getShotsScatter() { return this.#shotsScatter }; + getShotsIntensity() { return this.#shotsIntensity }; + getHealth() { return this.#health }; + + static getConstructor(type: string) { + if (type === "GroundUnit") return GroundUnit; + if (type === "Aircraft") return Aircraft; + if (type === "Helicopter") return Helicopter; + if (type === "NavyUnit") return NavyUnit; + } + + constructor(ID: number) { + super(new LatLng(0, 0), { riseOnHover: true, keyboard: false }); + + this.ID = ID; + + this.#pathPolyline = new Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1 }); + this.#pathPolyline.addTo(getApp().getMap()); + this.#targetPositionMarker = new TargetMarker(new LatLng(0, 0)); + this.#targetPositionPolyline = new Polyline([], { color: '#FF0000', weight: 3, opacity: 0.5, smoothFactor: 1 }); + this.#engagementCircle = new RangeCircle(this.getPosition(), { radius: 0, weight: 4, opacity: 1, fillOpacity: 0, dashArray: "4 8", interactive: false, bubblingMouseEvents: false }); + this.#acquisitionCircle = new RangeCircle(this.getPosition(), { radius: 0, weight: 2, opacity: 1, fillOpacity: 0, dashArray: "8 12", interactive: false, bubblingMouseEvents: false }); + + /* Leaflet events listeners */ + this.on('click', (e) => this.#onClick(e)); + this.on('dblclick', (e) => this.#onDoubleClick(e)); + this.on('contextmenu', (e) => this.#onContextMenu(e)); + this.on('mouseover', () => { + if (this.belongsToCommandedCoalition()) { + this.setHighlighted(true); + document.dispatchEvent(new CustomEvent("unitMouseover", { detail: this })); + } + }); + this.on('mouseout', () => { + this.setHighlighted(false); + document.dispatchEvent(new CustomEvent("unitMouseout", { detail: this })); + }); + getApp().getMap().on("zoomend", (e: any) => { this.#onZoom(e); }) + + /* Deselect units if they are hidden */ + document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { + this.#updateMarker(); + this.setSelected(this.getSelected() && !this.getHidden()); + }); + + document.addEventListener("toggleMarkerVisibility", (ev: CustomEventInit) => { + this.#updateMarker(); + this.setSelected(this.getSelected() && !this.getHidden()); + }); + + /* Update the marker when the options change */ + document.addEventListener("mapOptionsChanged", (ev: CustomEventInit) => { + this.#updateMarker(); + + /* Circles don't like to be updated when the map is zooming */ + if (!getApp().getMap().isZooming()) + this.#drawRanges(); + else + this.once("zoomend", () => { this.#drawRanges(); }) + + if (this.getSelected()) + this.drawLines(); + }); + } + + /********************** Abstract methods *************************/ + /** Get the unit category string + * + * @returns string The unit category + */ + abstract getCategory(): string; + + /** Get the icon options + * Used to configure how the marker appears on the map + * + * @returns ObjectIconOptions + */ + abstract getIconOptions(): ObjectIconOptions; + + /** Get the actions that this unit can perform + * + */ + abstract appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null): void; + + /** + * + * @returns string containing the marker category + */ + abstract getMarkerCategory(): string; + + /** + * + * @returns string containing the default marker + */ + abstract getDefaultMarker(): string; + + /** Get the category but for display use - for the user. (i.e. has spaces in it) + * + * @returns string + */ + getCategoryLabel() { + return ((GROUND_UNIT_AIR_DEFENCE_REGEX.test(this.getType())) ? "Air Defence" : this.getCategory()).replace(/([a-z])([A-Z])/g, "$1 $2"); + } + + /********************** Unit data *************************/ + /** This function is called by the units manager to update all the data coming from the backend. It reads the binary raw data using a DataExtractor + * + * @param dataExtractor The DataExtractor object pointing to the binary buffer which contains the raw data coming from the backend + */ + setData(dataExtractor: DataExtractor) { + /* This variable controls if the marker must be updated. This is not always true since not all variables have an effect on the marker */ + var updateMarker = !getApp().getMap().hasLayer(this); + + var oldIsLeader = this.#isLeader; + var datumIndex = 0; + while (datumIndex != DataIndexes.endOfData) { + datumIndex = dataExtractor.extractUInt8(); + switch (datumIndex) { + case DataIndexes.category: dataExtractor.extractString(); break; + case DataIndexes.alive: this.setAlive(dataExtractor.extractBool()); updateMarker = true; break; + case DataIndexes.human: this.#human = dataExtractor.extractBool(); break; + case DataIndexes.controlled: this.#controlled = dataExtractor.extractBool(); updateMarker = true; break; + case DataIndexes.coalition: let newCoalition = enumToCoalition(dataExtractor.extractUInt8()); updateMarker = true; if (newCoalition != this.#coalition) this.#clearRanges(); this.#coalition = newCoalition; break; // If the coalition has changed, redraw the range circles to update the colour + case DataIndexes.country: this.#country = dataExtractor.extractUInt8(); break; + case DataIndexes.name: this.#name = dataExtractor.extractString(); break; + case DataIndexes.unitName: this.#unitName = dataExtractor.extractString(); break; + case DataIndexes.groupName: this.#groupName = dataExtractor.extractString(); updateMarker = true; break; + case DataIndexes.state: this.#state = enumToState(dataExtractor.extractUInt8()); updateMarker = true; break; + case DataIndexes.task: this.#task = dataExtractor.extractString(); break; + case DataIndexes.hasTask: this.#hasTask = dataExtractor.extractBool(); break; + case DataIndexes.position: this.#position = dataExtractor.extractLatLng(); updateMarker = true; break; + case DataIndexes.speed: this.#speed = dataExtractor.extractFloat64(); updateMarker = true; break; + case DataIndexes.horizontalVelocity: this.#horizontalVelocity = dataExtractor.extractFloat64(); break; + case DataIndexes.verticalVelocity: this.#verticalVelocity = dataExtractor.extractFloat64(); break; + case DataIndexes.heading: this.#heading = dataExtractor.extractFloat64(); updateMarker = true; break; + case DataIndexes.track: this.#track = dataExtractor.extractFloat64(); updateMarker = true; break; + case DataIndexes.isActiveTanker: this.#isActiveTanker = dataExtractor.extractBool(); break; + case DataIndexes.isActiveAWACS: this.#isActiveAWACS = dataExtractor.extractBool(); break; + case DataIndexes.onOff: this.#onOff = dataExtractor.extractBool(); break; + case DataIndexes.followRoads: this.#followRoads = dataExtractor.extractBool(); break; + case DataIndexes.fuel: this.#fuel = dataExtractor.extractUInt16(); break; + case DataIndexes.desiredSpeed: this.#desiredSpeed = dataExtractor.extractFloat64(); break; + case DataIndexes.desiredSpeedType: this.#desiredSpeedType = dataExtractor.extractBool() ? "GS" : "CAS"; break; + case DataIndexes.desiredAltitude: this.#desiredAltitude = dataExtractor.extractFloat64(); break; + case DataIndexes.desiredAltitudeType: this.#desiredAltitudeType = dataExtractor.extractBool() ? "AGL" : "ASL"; break; + case DataIndexes.leaderID: this.#leaderID = dataExtractor.extractUInt32(); break; + case DataIndexes.formationOffset: this.#formationOffset = dataExtractor.extractOffset(); break; + case DataIndexes.targetID: this.#targetID = dataExtractor.extractUInt32(); break; + case DataIndexes.targetPosition: this.#targetPosition = dataExtractor.extractLatLng(); break; + case DataIndexes.ROE: this.#ROE = enumToROE(dataExtractor.extractUInt8()); break; + case DataIndexes.reactionToThreat: this.#reactionToThreat = enumToReactionToThreat(dataExtractor.extractUInt8()); break; + case DataIndexes.emissionsCountermeasures: this.#emissionsCountermeasures = enumToEmissioNCountermeasure(dataExtractor.extractUInt8()); break; + case DataIndexes.TACAN: this.#TACAN = dataExtractor.extractTACAN(); break; + case DataIndexes.radio: this.#radio = dataExtractor.extractRadio(); break; + case DataIndexes.generalSettings: this.#generalSettings = dataExtractor.extractGeneralSettings(); break; + case DataIndexes.ammo: this.#ammo = dataExtractor.extractAmmo(); break; + case DataIndexes.contacts: this.#contacts = dataExtractor.extractContacts(); document.dispatchEvent(new CustomEvent("contactsUpdated", { detail: this })); break; + case DataIndexes.activePath: this.#activePath = dataExtractor.extractActivePath(); break; + case DataIndexes.isLeader: this.#isLeader = dataExtractor.extractBool(); break; + case DataIndexes.operateAs: this.#operateAs = enumToCoalition(dataExtractor.extractUInt8()); break; + case DataIndexes.shotsScatter: this.#shotsScatter = dataExtractor.extractUInt8(); break; + case DataIndexes.shotsIntensity: this.#shotsIntensity = dataExtractor.extractUInt8(); break; + case DataIndexes.health: this.#health = dataExtractor.extractUInt8(); updateMarker = true; break; + } + } + + /* Dead and hidden units can't be selected */ + this.setSelected(this.getSelected() && this.#alive && !this.getHidden()) + + /* Update the marker if required */ + if (updateMarker) + this.#updateMarker(); + + /* Redraw the marker if isLeader has changed. TODO I don't love this approach, observables may be more elegant */ + if (oldIsLeader !== this.#isLeader) { + this.#redrawMarker(); + + /* Reapply selection */ + if (this.getSelected()) { + this.setSelected(false); + this.setSelected(true); + } + } + + /* If the unit is selected or if the view is centered on this unit, sent the update signal so that other elements like the UnitControlPanel can be updated. */ + if (this.getSelected() || getApp().getMap().getCenteredOnUnit() === this) + document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this })); + } + + /** Get unit data collated into an object + * + * @returns object populated by unit information which can also be retrieved using getters + */ + getData(): UnitData { + return { + category: this.getCategory(), + categoryDisplayName: this.getCategoryLabel(), + ID: this.ID, + alive: this.#alive, + human: this.#human, + controlled: this.#controlled, + coalition: this.#coalition, + country: this.#country, + name: this.#name, + unitName: this.#unitName, + groupName: this.#groupName, + state: this.#state, + task: this.#task, + hasTask: this.#hasTask, + position: this.#position, + speed: this.#speed, + horizontalVelocity: this.#horizontalVelocity, + verticalVelocity: this.#verticalVelocity, + heading: this.#heading, + track: this.#track, + isActiveTanker: this.#isActiveTanker, + isActiveAWACS: this.#isActiveAWACS, + onOff: this.#onOff, + followRoads: this.#followRoads, + fuel: this.#fuel, + desiredSpeed: this.#desiredSpeed, + desiredSpeedType: this.#desiredSpeedType, + desiredAltitude: this.#desiredAltitude, + desiredAltitudeType: this.#desiredAltitudeType, + leaderID: this.#leaderID, + formationOffset: this.#formationOffset, + targetID: this.#targetID, + targetPosition: this.#targetPosition, + ROE: this.#ROE, + reactionToThreat: this.#reactionToThreat, + emissionsCountermeasures: this.#emissionsCountermeasures, + TACAN: this.#TACAN, + radio: this.#radio, + generalSettings: this.#generalSettings, + ammo: this.#ammo, + contacts: this.#contacts, + activePath: this.#activePath, + isLeader: this.#isLeader, + operateAs: this.#operateAs, + shotsScatter: this.#shotsScatter, + shotsIntensity: this.#shotsIntensity, + health: this.#health + } + } + + /** Get a database of information also in this unit's category + * + * @returns UnitDatabase + */ + getDatabase(): UnitDatabase | null { + return getUnitDatabaseByCategory(this.getMarkerCategory()); + } + + /** Set the unit as alive or dead + * + * @param newAlive (boolean) true = alive, false = dead + */ + setAlive(newAlive: boolean) { + if (newAlive != this.#alive) + document.dispatchEvent(new CustomEvent("unitDeath", { detail: this })); + this.#alive = newAlive; + } + + /** Set the unit as user-selected + * + * @param selected (boolean) + */ + setSelected(selected: boolean) { + /* Only alive units can be selected that belong to the commanded coalition can be selected */ + if ((this.#alive || !selected) && this.belongsToCommandedCoalition() && this.getSelected() != selected) { + this.#selected = selected; + + /* If selected, update the marker to show the selected effects, else clear all the drawings that are only shown for selected units. */ + if (selected) { + this.#updateMarker(); + } + else { + this.#clearContacts(); + this.#clearPath(); + this.#clearTargetPosition(); + } + + /* When the group leader is selected, if grouping is active, all the other group members are also selected */ + if (this.getCategory() === "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION) { + if (this.#isLeader) { + /* Redraw the marker in case the leader unit was replaced by a group marker, like for SAM Sites */ + this.#redrawMarker(); + this.getGroupMembers().forEach((unit: Unit) => unit.setSelected(selected)); + } + else { + this.#updateMarker(); + } + } + + /* Activate the selection effects on the marker */ + this.getElement()?.querySelector(`.unit`)?.toggleAttribute("data-is-selected", selected); + + /* Trigger events after all (de-)selecting has been done */ + if (selected) { + document.dispatchEvent(new CustomEvent("unitSelection", { detail: this })); + } else { + document.dispatchEvent(new CustomEvent("unitDeselection", { detail: this })); + } + } + } + + /** Is this unit selected? + * + * @returns boolean + */ + getSelected() { + return this.#selected; + } + + /** Set the number of the hotgroup to which the unit belongss + * + * @param hotgroup (number) + */ + setHotgroup(hotgroup: number | null) { + this.#hotgroup = hotgroup; + this.#updateMarker(); + } + + /** Get the unit's hotgroup number + * + * @returns number + */ + getHotgroup() { + return this.#hotgroup; + } + + /** Set the unit as highlighted + * + * @param highlighted (boolean) + */ + setHighlighted(highlighted: boolean) { + if (this.#highlighted != highlighted) { + this.#highlighted = highlighted; + this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-highlighted", highlighted); + this.getGroupMembers().forEach((unit: Unit) => unit.setHighlighted(highlighted)); + } + } + + /** Get whether the unit is highlighted or not + * + * @returns boolean + */ + getHighlighted() { + return this.#highlighted; + } + + /** Get the other members of the group which this unit is in + * + * @returns Unit[] + */ + getGroupMembers() { + if (this.#group !== null) + return this.#group.getMembers().filter((unit: Unit) => { return unit != this; }) + return []; + } + + /** Return the leader of the group + * + * @returns Unit The leader of the group + */ + getGroupLeader() { + if (this.#group !== null) + return this.#group.getLeader(); + return null; + } + + /** Returns whether the user is allowed to command this unit, based on coalition + * + * @returns boolean + */ + belongsToCommandedCoalition() { + return (getApp().getMissionManager().getCommandModeOptions().commandMode !== GAME_MASTER && getApp().getMissionManager().getCommandedCoalition() !== this.#coalition) ? false : true; + } + + getType() { + return ""; + } + + getSpawnPoints() { + return this.getDatabase()?.getSpawnPointsByName(this.getName()); + } + + getDatabaseEntry() { + return this.getDatabase()?.getByName(this.#name) ?? this.getDatabase()?.getUnkownUnit(this.getName()); + } + + getGroup() { + return this.#group; + } + + setGroup(group: Group | null) { + this.#group = group; + } + + drawLines() { + /* Leaflet does not like it when you change coordinates when the map is zooming */ + if (!getApp().getMap().isZooming()) { + this.#drawPath(); + this.#drawContacts(); + this.#drawTarget(); + } + } + + checkZoomRedraw() { + return false; + } + + isControlledByDCS() { + return this.getControlled() === false && this.getHuman() === false; + } + + isControlledByOlympus() { + return this.getControlled() === true; + } + + /********************** Icon *************************/ + createIcon(): void { + /* Set the icon */ + var icon = new DivIcon({ + className: 'leaflet-unit-icon', + iconAnchor: [25, 25], + iconSize: [50, 50], + }); + this.setIcon(icon); + + /* Create the base element */ + var el = document.createElement("div"); + el.classList.add("unit"); + el.setAttribute("data-object", `unit-${this.getMarkerCategory()}`); + el.setAttribute("data-coalition", this.#coalition); + + var iconOptions = this.getIconOptions(); + + /* Generate and append elements depending on active options */ + /* Velocity vector */ + if (iconOptions.showVvi) { + var vvi = document.createElement("div"); + vvi.classList.add("unit-vvi"); + vvi.toggleAttribute("data-rotate-to-heading"); + el.append(vvi); + } + + /* Hotgroup indicator */ + if (iconOptions.showHotgroup) { + var hotgroup = document.createElement("div"); + hotgroup.classList.add("unit-hotgroup"); + var hotgroupId = document.createElement("div"); + hotgroupId.classList.add("unit-hotgroup-id"); + hotgroup.appendChild(hotgroupId); + el.append(hotgroup); + } + + /* Main icon */ + if (iconOptions.showUnitIcon) { + var unitIcon = document.createElement("div"); + unitIcon.classList.add("unit-icon"); + var img = document.createElement("img"); + + /* If a unit does not belong to the commanded coalition or it is not visually detected, show it with the generic aircraft square */ + var marker; + if (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value))) + marker = this.getDatabaseEntry()?.markerFile ?? this.getDefaultMarker(); + else + marker = "aircraft"; + img.src = `/resources/theme/images/units/${marker}.svg`; + img.onload = () => SVGInjector(img); + unitIcon.appendChild(img); + + unitIcon.toggleAttribute("data-rotate-to-heading", iconOptions.rotateToHeading); + el.append(unitIcon); + } + + /* State icon */ + if (iconOptions.showState) { + var state = document.createElement("div"); + state.classList.add("unit-state"); + el.appendChild(state); + } + + /* Short label */ + if (iconOptions.showShortLabel) { + var shortLabel = document.createElement("div"); + shortLabel.classList.add("unit-short-label"); + shortLabel.innerText = this.getDatabaseEntry()?.shortLabel || ""; + el.append(shortLabel); + } + + /* Fuel indicator */ + if (iconOptions.showFuel) { + var fuelIndicator = document.createElement("div"); + fuelIndicator.classList.add("unit-fuel"); + var fuelLevel = document.createElement("div"); + fuelLevel.classList.add("unit-fuel-level"); + fuelIndicator.appendChild(fuelLevel); + el.append(fuelIndicator); + } + + /* Health indicator */ + if (iconOptions.showHealth) { + var healthIndicator = document.createElement("div"); + healthIndicator.classList.add("unit-health"); + var healthLevel = document.createElement("div"); + healthLevel.classList.add("unit-health-level"); + healthIndicator.appendChild(healthLevel); + el.append(healthIndicator); + } + + /* Ammo indicator */ + if (iconOptions.showAmmo) { + var ammoIndicator = document.createElement("div"); + ammoIndicator.classList.add("unit-ammo"); + for (let i = 0; i <= 3; i++) + ammoIndicator.appendChild(document.createElement("div")); + el.append(ammoIndicator); + } + + /* Unit summary */ + if (iconOptions.showSummary) { + var summary = document.createElement("div"); + summary.classList.add("unit-summary"); + var callsign = document.createElement("div"); + callsign.classList.add("unit-callsign"); + callsign.innerText = this.#unitName; + var altitude = document.createElement("div"); + altitude.classList.add("unit-altitude"); + var speed = document.createElement("div"); + speed.classList.add("unit-speed"); + if (iconOptions.showCallsign) summary.appendChild(callsign); + summary.appendChild(altitude); + summary.appendChild(speed); + el.appendChild(summary); + } + + this.getElement()?.appendChild(el); + } + + /********************** Visibility *************************/ + updateVisibility() { + const hiddenTypes = getApp().getMap().getHiddenTypes(); + var hidden = ( + /* Hide the unit if it is a human and humans are hidden */ + (this.getHuman() && hiddenTypes.includes("human")) || + /* Hide the unit if it is DCS-controlled and DCS controlled units are hidden */ + (this.isControlledByDCS() && hiddenTypes.includes("dcs")) || + /* Hide the unit if it is Olympus-controlled and Olympus-controlled units are hidden */ + (this.isControlledByOlympus() && hiddenTypes.includes("olympus")) || + /* Hide the unit if this specific category is hidden */ + (hiddenTypes.includes(this.getMarkerCategory())) || + /* Hide the unit if this coalition is hidden */ + (hiddenTypes.includes(this.#coalition)) || + /* 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().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && !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 */ + this.setHidden(hidden || !this.getAlive()); + } + + setHidden(hidden: boolean) { + this.#hidden = hidden; + + /* Add the marker if not present */ + if (!getApp().getMap().hasLayer(this) && !this.getHidden()) { + if (getApp().getMap().isZooming()) + this.once("zoomend", () => { this.addTo(getApp().getMap()) }) + else + this.addTo(getApp().getMap()); + } + + /* Hide the marker if necessary*/ + if (getApp().getMap().hasLayer(this) && this.getHidden()) { + getApp().getMap().removeLayer(this); + } + + /* Draw the range circles if the unit is not hidden */ + if (!this.getHidden()) { + /* Circles don't like to be updated when the map is zooming */ + if (!getApp().getMap().isZooming()) + this.#drawRanges(); + else + this.once("zoomend", () => { this.#drawRanges(); }) + } else { + this.#clearRanges(); + } + } + + getHidden() { + return this.#hidden; + } + + setDetectionMethods(newDetectionMethods: number[]) { + if (!this.belongsToCommandedCoalition()) { + /* Check if the detection methods of this unit have changed */ + if (this.#detectionMethods.length !== newDetectionMethods.length || this.getDetectionMethods().some(value => !newDetectionMethods.includes(value))) { + /* Force a redraw of the unit to reflect the new status of the detection methods */ + this.setHidden(true); + this.#detectionMethods = newDetectionMethods; + this.#updateMarker(); + } + } + } + + getDetectionMethods() { + return this.#detectionMethods; + } + + getLeader() { + return getApp().getUnitsManager().getUnitByID(this.#leaderID); + } + + canFulfillRole(roles: string | string[]) { + if (typeof (roles) === "string") + roles = [roles]; + + var loadouts = this.getDatabaseEntry()?.loadouts; + if (loadouts) { + return loadouts.some((loadout: LoadoutBlueprint) => { + return (roles as string[]).some((role: string) => { return loadout.roles.includes(role) }); + }); + } else + return false; + } + + isInViewport() { + return getApp().getMap().getBounds().contains(this.getPosition()); + } + + canTargetPoint() { + return this.getDatabaseEntry()?.canTargetPoint === true; + } + + canRearm() { + return this.getDatabaseEntry()?.canRearm === true; + } + + canAAA() { + return this.getDatabaseEntry()?.canAAA === true; + } + + isIndirectFire() { + return this.getDatabaseEntry()?.indirectFire === true; + } + + isTanker() { + return this.canFulfillRole("Tanker"); + } + + isAWACS() { + return this.canFulfillRole("AWACS"); + } + + /********************** Unit commands *************************/ + addDestination(latlng: L.LatLng) { + if (!this.#human) { + var path: any = {}; + if (this.#activePath.length > 0) { + path = this.#activePath; + path[(Object.keys(path).length).toString()] = latlng; + } + else { + path = [latlng]; + } + getApp().getServerManager().addDestination(this.ID, path); + } + } + + clearDestinations() { + if (!this.#human) + this.#activePath = []; + } + + attackUnit(targetID: number) { + /* Units can't attack themselves */ + if (!this.#human) + if (this.ID != targetID) + getApp().getServerManager().attackUnit(this.ID, targetID); + } + + followUnit(targetID: number, offset: { "x": number, "y": number, "z": number }) { + /* Units can't follow themselves */ + if (!this.#human) + if (this.ID != targetID) + getApp().getServerManager().followUnit(this.ID, targetID, offset); + } + + landAt(latlng: LatLng) { + if (!this.#human) + getApp().getServerManager().landAt(this.ID, latlng); + } + + changeSpeed(speedChange: string) { + if (!this.#human) + getApp().getServerManager().changeSpeed(this.ID, speedChange); + } + + changeAltitude(altitudeChange: string) { + if (!this.#human) + getApp().getServerManager().changeAltitude(this.ID, altitudeChange); + } + + setSpeed(speed: number) { + if (!this.#human) + getApp().getServerManager().setSpeed(this.ID, speed); + } + + setSpeedType(speedType: string) { + if (!this.#human) + getApp().getServerManager().setSpeedType(this.ID, speedType); + } + + setAltitude(altitude: number) { + if (!this.#human) + getApp().getServerManager().setAltitude(this.ID, altitude); + } + + setAltitudeType(altitudeType: string) { + if (!this.#human) + getApp().getServerManager().setAltitudeType(this.ID, altitudeType); + } + + setROE(ROE: string) { + if (!this.#human) + getApp().getServerManager().setROE(this.ID, ROE); + } + + setReactionToThreat(reactionToThreat: string) { + if (!this.#human) + getApp().getServerManager().setReactionToThreat(this.ID, reactionToThreat); + } + + setEmissionsCountermeasures(emissionCountermeasure: string) { + if (!this.#human) + getApp().getServerManager().setEmissionsCountermeasures(this.ID, emissionCountermeasure); + } + + setOnOff(onOff: boolean) { + if (!this.#human) + getApp().getServerManager().setOnOff(this.ID, onOff); + } + + setFollowRoads(followRoads: boolean) { + if (!this.#human) + getApp().getServerManager().setFollowRoads(this.ID, followRoads); + } + + setOperateAs(operateAs: string) { + if (!this.#human) + getApp().getServerManager().setOperateAs(this.ID, coalitionToEnum(operateAs)); + } + + delete(explosion: boolean, explosionType: string, immediate: boolean) { + getApp().getServerManager().deleteUnit(this.ID, explosion, explosionType, immediate); + } + + refuel() { + if (!this.#human) + getApp().getServerManager().refuel(this.ID); + } + + setAdvancedOptions(isActiveTanker: boolean, isActiveAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) { + if (!this.#human) + getApp().getServerManager().setAdvacedOptions(this.ID, isActiveTanker, isActiveAWACS, TACAN, radio, generalSettings); + } + + bombPoint(latlng: LatLng) { + getApp().getServerManager().bombPoint(this.ID, latlng); + } + + carpetBomb(latlng: LatLng) { + getApp().getServerManager().carpetBomb(this.ID, latlng); + } + + bombBuilding(latlng: LatLng) { + getApp().getServerManager().bombBuilding(this.ID, latlng); + } + + fireAtArea(latlng: LatLng) { + getApp().getServerManager().fireAtArea(this.ID, latlng); + } + + simulateFireFight(latlng: LatLng, targetGroundElevation: number | null) { + getGroundElevation(this.getPosition(), (response: string) => { + var unitGroundElevation: number | null = null; + try { + unitGroundElevation = parseFloat(response); + } catch { + console.log("Simulate fire fight: could not retrieve ground elevation") + } + + /* DCS and SRTM altitude data is not exactly the same so to minimize error we use SRTM only to compute relative elevation difference */ + var altitude = this.getPosition().alt; + if (altitude !== undefined && targetGroundElevation !== null && unitGroundElevation !== null) + getApp().getServerManager().simulateFireFight(this.ID, latlng, altitude + targetGroundElevation - unitGroundElevation); + }); + } + + // TODO: Remove coalition + scenicAAA() { + var coalition = "neutral"; + if (this.getCoalition() === "red") + coalition = "blue"; + else if (this.getCoalition() == "blue") + coalition = "red"; + getApp().getServerManager().scenicAAA(this.ID, coalition); + } + + // TODO: Remove coalition + missOnPurpose() { + var coalition = "neutral"; + if (this.getCoalition() === "red") + coalition = "blue"; + else if (this.getCoalition() == "blue") + coalition = "red"; + getApp().getServerManager().missOnPurpose(this.ID, coalition); + } + + landAtPoint(latlng: LatLng) { + getApp().getServerManager().landAtPoint(this.ID, latlng); + } + + setShotsScatter(shotsScatter: number) { + if (!this.#human) + getApp().getServerManager().setShotsScatter(this.ID, shotsScatter); + } + + setShotsIntensity(shotsIntensity: number) { + if (!this.#human) + getApp().getServerManager().setShotsIntensity(this.ID, shotsIntensity); + } + + /***********************************************/ + onAdd(map: Map): this { + super.onAdd(map); + return this; + } + + onGroupChanged(member: Unit) { + this.#redrawMarker(); + } + + showFollowOptions(units: Unit[]) { + var contextActionSet = new ContextActionSet(); + + contextActionSet.addContextAction(this, 'trail', "Trail", "Follow unit in trail formation", () => this.applyFollowOptions('trail', units)); + contextActionSet.addContextAction(this, 'echelon-lh', "Echelon (LH)", "Follow unit in echelon left formation", () => this.applyFollowOptions('echelon-lh', units)); + contextActionSet.addContextAction(this, 'echelon-rh', "Echelon (RH)", "Follow unit in echelon right formation", () => this.applyFollowOptions('echelon-rh', units)); + contextActionSet.addContextAction(this, 'line-abreast-lh', "Line abreast (LH)", "Follow unit in line abreast left formation", () => this.applyFollowOptions('line-abreast-lh', units)); + contextActionSet.addContextAction(this, 'line-abreast-rh', "Line abreast (RH)", "Follow unit in line abreast right formation", () => this.applyFollowOptions('line-abreast-rh', units)); + contextActionSet.addContextAction(this, 'front', "Front", "Fly in front of unit", () => this.applyFollowOptions('front', units)); + contextActionSet.addContextAction(this, 'diamond', "Diamond", "Follow unit in diamond formation", () => this.applyFollowOptions('diamond', units)); + contextActionSet.addContextAction(this, 'custom', "Custom", "Set a custom formation position", () => this.applyFollowOptions('custom', units)); + + //getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet); + getApp().getMap().showUnitContextMenu(); + } + + applyFollowOptions(formation: string, units: Unit[]) { + if (formation === "custom") { + document.getElementById("custom-formation-dialog")?.classList.remove("hide"); + document.addEventListener("applyCustomFormation", () => { + var dialog = document.getElementById("custom-formation-dialog"); + if (dialog) { + dialog.classList.add("hide"); + var clock = 1; + while (clock < 8) { + if ((dialog.querySelector(`#formation-${clock}`)).checked) + break + clock++; + } + var angleDeg = 360 - (clock - 1) * 45; + var angleRad = deg2rad(angleDeg); + var distance = ftToM(parseInt((dialog.querySelector(`#distance`)?.querySelector("input")).value)); + var upDown = ftToM(parseInt((dialog.querySelector(`#up-down`)?.querySelector("input")).value)); + + // X: front-rear, positive front + // Y: top-bottom, positive top + // Z: left-right, positive right + var x = distance * Math.cos(angleRad); + var y = upDown; + var z = distance * Math.sin(angleRad); + + getApp().getUnitsManager().followUnit(this.ID, { "x": x, "y": y, "z": z }, undefined, units); + } + }); + } + else { + getApp().getUnitsManager().followUnit(this.ID, undefined, formation, units); + } + } + + /***********************************************/ + #onClick(e: any) { + /* Exit if we were waiting for a doubleclick */ + if (this.#waitingForDoubleClick) { + return; + } + + /* We'll wait for a doubleclick */ + this.#waitingForDoubleClick = true; + this.#doubleClickTimer = window.setTimeout(() => { + /* Still waiting so no doubleclick; do the click action */ + if (this.#waitingForDoubleClick) { + if (getApp().getMap().getState() === IDLE || getApp().getMap().getState() === MOVE_UNIT || e.originalEvent.ctrlKey) { + if (!e.originalEvent.ctrlKey) + getApp().getUnitsManager().deselectAllUnits(); + + this.setSelected(!this.getSelected()); + } + } + + /* No longer waiting for a doubleclick */ + this.#waitingForDoubleClick = false; + }, 200); + } + + #onDoubleClick(e: any) { + /* Let single clicks work again */ + this.#waitingForDoubleClick = false; + clearTimeout(this.#doubleClickTimer); + + /* Select all matching units in the viewport */ + const unitsManager = getApp().getUnitsManager(); + Object.values(unitsManager.getUnits()).forEach((unit: Unit) => { + if (unit.getAlive() === true && unit.getName() === this.getName() && unit.isInViewport()) + unitsManager.selectUnit(unit.ID, false); + }); + } + + #onContextMenu(e: any) { + var contextActionSet = new ContextActionSet(); + + var units = getApp().getUnitsManager().getSelectedUnits(); + if (!units.includes(this)) + units.push(this); + + units.forEach((unit: Unit) => { + unit.appendContextActions(contextActionSet, this, null); + }) + + if (Object.keys(contextActionSet.getContextActions()).length > 0) { + getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); + //getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet); + } + } + + #updateMarker() { + this.updateVisibility(); + + /* Draw the minimap marker */ + var drawMiniMapMarker = (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))); + if (this.#alive && drawMiniMapMarker) { + if (this.#miniMapMarker == null) { + this.#miniMapMarker = new CircleMarker(new LatLng(this.#position.lat, this.#position.lng), { radius: 0.5 }); + if (this.#coalition == "neutral") + this.#miniMapMarker.setStyle({ color: "#CFD9E8" }); + else if (this.#coalition == "red") + this.#miniMapMarker.setStyle({ color: "#ff5858" }); + else + this.#miniMapMarker.setStyle({ color: "#247be2" }); + this.#miniMapMarker.addTo(getApp().getMap().getMiniMapLayerGroup()); + this.#miniMapMarker.bringToBack(); + } + else { + if (this.#miniMapMarker.getLatLng().lat !== this.getPosition().lat || this.#miniMapMarker.getLatLng().lng !== this.getPosition().lng) { + this.#miniMapMarker.setLatLng(new LatLng(this.#position.lat, this.#position.lng)); + this.#miniMapMarker.bringToBack(); + } + } + } + else { + if (this.#miniMapMarker != null && getApp().getMap().getMiniMapLayerGroup().hasLayer(this.#miniMapMarker)) { + getApp().getMap().getMiniMapLayerGroup().removeLayer(this.#miniMapMarker); + this.#miniMapMarker = null; + } + } + + /* Draw the marker */ + if (!this.getHidden()) { + if (this.getLatLng().lat !== this.#position.lat || this.getLatLng().lng !== this.#position.lng) { + this.setLatLng(new LatLng(this.#position.lat, this.#position.lng)); + } + + var element = this.getElement(); + if (element != null) { + /* Draw the velocity vector */ + element.querySelector(".unit-vvi")?.setAttribute("style", `height: ${15 + this.#speed / 5}px;`); + + /* Set fuel data */ + element.querySelector(".unit-fuel-level")?.setAttribute("style", `width: ${this.#fuel}%`); + element.querySelector(".unit")?.toggleAttribute("data-has-low-fuel", this.#fuel < 20); + + /* Set health data */ + element.querySelector(".unit-health-level")?.setAttribute("style", `width: ${this.#health}%`); + element.querySelector(".unit")?.toggleAttribute("data-has-low-health", this.#health < 20); + + /* Set dead/alive flag */ + element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.#alive); + + /* Set current unit state */ + if (this.#human) { // Unit is human + element.querySelector(".unit")?.setAttribute("data-state", "human"); + } + else if (!this.#controlled) { // Unit is under DCS control (not Olympus) + element.querySelector(".unit")?.setAttribute("data-state", "dcs"); + } + else if ((this.getCategory() == "Aircraft" || this.getCategory() == "Helicopter") && !this.#hasTask) { + element.querySelector(".unit")?.setAttribute("data-state", "no-task"); + } + else { // Unit is under Olympus control + if (this.#onOff) { + if (this.#isActiveTanker) + element.querySelector(".unit")?.setAttribute("data-state", "tanker"); + else if (this.#isActiveAWACS) + element.querySelector(".unit")?.setAttribute("data-state", "AWACS"); + else + element.querySelector(".unit")?.setAttribute("data-state", this.#state.toLowerCase()); + } + else { + element.querySelector(".unit")?.setAttribute("data-state", "off"); + } + } + + /* Set altitude and speed */ + if (element.querySelector(".unit-altitude")) + (element.querySelector(".unit-altitude")).innerText = "FL" + zeroAppend(Math.floor(mToFt(this.#position.alt as number) / 100), 3); + if (element.querySelector(".unit-speed")) + (element.querySelector(".unit-speed")).innerText = String(Math.floor(msToKnots(this.#speed))) + "GS"; + + /* Rotate elements according to heading */ + element.querySelectorAll("[data-rotate-to-heading]").forEach(el => { + const headingDeg = rad2deg(this.#track); + let currentStyle = el.getAttribute("style") || ""; + el.setAttribute("style", currentStyle + `transform:rotate(${headingDeg}deg);`); + }); + + /* Turn on ammo indicators */ + var hasFox1 = element.querySelector(".unit")?.hasAttribute("data-has-fox-1"); + var hasFox2 = element.querySelector(".unit")?.hasAttribute("data-has-fox-2"); + var hasFox3 = element.querySelector(".unit")?.hasAttribute("data-has-fox-3"); + var hasOtherAmmo = element.querySelector(".unit")?.hasAttribute("data-has-other-ammo"); + + var newHasFox1 = false; + var newHasFox2 = false; + var newHasFox3 = false; + var newHasOtherAmmo = false; + Object.values(this.#ammo).forEach((ammo: Ammo) => { + if (ammo.category == 1 && ammo.missileCategory == 1) { + if (ammo.guidance == 4 || ammo.guidance == 5) + newHasFox1 = true; + else if (ammo.guidance == 2) + newHasFox2 = true; + else if (ammo.guidance == 3) + newHasFox3 = true; + } + else + newHasOtherAmmo = true; + }); + + if (hasFox1 != newHasFox1) element.querySelector(".unit")?.toggleAttribute("data-has-fox-1", newHasFox1); + if (hasFox2 != newHasFox2) element.querySelector(".unit")?.toggleAttribute("data-has-fox-2", newHasFox2); + if (hasFox3 != newHasFox3) element.querySelector(".unit")?.toggleAttribute("data-has-fox-3", newHasFox3); + if (hasOtherAmmo != newHasOtherAmmo) element.querySelector(".unit")?.toggleAttribute("data-has-other-ammo", newHasOtherAmmo); + + /* Draw the hotgroup element */ + element.querySelector(".unit")?.toggleAttribute("data-is-in-hotgroup", this.#hotgroup != null); + if (this.#hotgroup) { + const hotgroupEl = element.querySelector(".unit-hotgroup-id") as HTMLElement; + if (hotgroupEl) + hotgroupEl.innerText = String(this.#hotgroup); + } + } + + /* Set vertical offset for altitude stacking */ + var pos = getApp().getMap().latLngToLayerPoint(this.getLatLng()).round(); + this.setZIndexOffset(1000 + Math.floor(this.#position.alt as number) - pos.y + (this.#highlighted || this.#selected ? 5000 : 0)); + } + } + + #redrawMarker() { + this.removeFrom(getApp().getMap()); + this.#updateMarker(); + + /* Activate the selection effects on the marker */ + this.getElement()?.querySelector(`.unit`)?.toggleAttribute("data-is-selected", this.getSelected()); + } + + #drawPath() { + if (this.#activePath != undefined && getApp().getMap().getVisibilityOptions()[SHOW_UNIT_PATHS]) { + var points: LatLng[] = []; + points.push(new LatLng(this.#position.lat, this.#position.lng)); + + /* Add markers if missing */ + while (this.#pathMarkers.length < Object.keys(this.#activePath).length) { + var marker = new Marker([0, 0], { icon: pathIcon }).addTo(getApp().getMap()); + this.#pathMarkers.push(marker); + } + + /* Remove markers if too many */ + while (this.#pathMarkers.length > Object.keys(this.#activePath).length) { + getApp().getMap().removeLayer(this.#pathMarkers[this.#pathMarkers.length - 1]); + this.#pathMarkers.splice(this.#pathMarkers.length - 1, 1) + } + + /* Update the position of the existing markers (to avoid creating markers uselessly) */ + for (let WP in this.#activePath) { + var destination = this.#activePath[WP]; + this.#pathMarkers[parseInt(WP)].setLatLng([destination.lat, destination.lng]); + points.push(new LatLng(destination.lat, destination.lng)); + this.#pathPolyline.setLatLngs(points); + } + + if (points.length == 1) + this.#clearPath(); + } + else { + this.#clearPath(); + } + } + + #clearPath() { + if (this.#pathPolyline.getLatLngs().length != 0) { + for (let WP in this.#pathMarkers) { + getApp().getMap().removeLayer(this.#pathMarkers[WP]); + } + this.#pathMarkers = []; + this.#pathPolyline.setLatLngs([]); + } + } + + #drawContacts() { + this.#clearContacts(); + if (getApp().getMap().getVisibilityOptions()[SHOW_UNIT_CONTACTS]) { + for (let index in this.#contacts) { + var contactData = this.#contacts[index]; + var contact: Unit | Weapon | null; + + if (contactData.ID in getApp().getUnitsManager().getUnits()) + contact = getApp().getUnitsManager().getUnitByID(contactData.ID); + else + contact = getApp().getWeaponsManager().getWeaponByID(contactData.ID); + + if (contact != null && contact.getAlive()) { + var startLatLng = new LatLng(this.#position.lat, this.#position.lng); + var endLatLng: LatLng; + if (contactData.detectionMethod === RWR) { + var bearingToContact = bearing(this.#position.lat, this.#position.lng, contact.getPosition().lat, contact.getPosition().lng); + var startXY = getApp().getMap().latLngToContainerPoint(startLatLng); + var endX = startXY.x + 80 * Math.sin(deg2rad(bearingToContact)); + var endY = startXY.y - 80 * Math.cos(deg2rad(bearingToContact)); + endLatLng = getApp().getMap().containerPointToLatLng(new Point(endX, endY)); + } + else + endLatLng = new LatLng(contact.getPosition().lat, contact.getPosition().lng); + + var color; + if (contactData.detectionMethod === VISUAL || contactData.detectionMethod === OPTIC) + color = "#FF00FF"; + else if (contactData.detectionMethod === RADAR || contactData.detectionMethod === IRST) + color = "#FFFF00"; + else if (contactData.detectionMethod === RWR) + color = "#00FF00"; + else + color = "#FFFFFF"; + var contactPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 1, smoothFactor: 1, dashArray: "4, 8" }); + contactPolyline.addTo(getApp().getMap()); + this.#contactsPolylines.push(contactPolyline) + } + } + } + } + + #clearContacts() { + for (let index in this.#contactsPolylines) { + getApp().getMap().removeLayer(this.#contactsPolylines[index]) + } + } + + #drawRanges() { + var engagementRange = 0; + var acquisitionRange = 0; + + /* Get the acquisition and engagement ranges of the entire group, not for each unit */ + if (this.getIsLeader()) { + var engagementRange = this.getDatabase()?.getByName(this.getName())?.engagementRange ?? 0; + var acquisitionRange = this.getDatabase()?.getByName(this.getName())?.acquisitionRange ?? 0; + + this.getGroupMembers().forEach((unit: Unit) => { + if (unit.getAlive()) { + let unitEngagementRange = unit.getDatabase()?.getByName(unit.getName())?.engagementRange ?? 0; + let unitAcquisitionRange = unit.getDatabase()?.getByName(unit.getName())?.acquisitionRange ?? 0; + + if (unitEngagementRange > engagementRange) + engagementRange = unitEngagementRange; + + if (unitAcquisitionRange > acquisitionRange) + acquisitionRange = unitAcquisitionRange; + } + }) + + if (acquisitionRange !== this.#acquisitionCircle.getRadius()) { + this.#acquisitionCircle.setRadius(acquisitionRange); + } + + if (engagementRange !== this.#engagementCircle.getRadius()) + this.#engagementCircle.setRadius(engagementRange); + + this.#engagementCircle.options.fillOpacity = this.getSelected() && getApp().getMap().getVisibilityOptions()[FILL_SELECTED_RING] ? 0.3 : 0; + + /* Acquisition circles */ + var shortAcquisitionRangeCheck = (acquisitionRange > nmToM(3) || !getApp().getMap().getVisibilityOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]); + + if (getApp().getMap().getVisibilityOptions()[SHOW_UNITS_ACQUISITION_RINGS] && 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()) { + case "red": + this.#acquisitionCircle.options.color = "#D42121"; + break; + case "blue": + this.#acquisitionCircle.options.color = "#017DC1"; + break; + default: + this.#acquisitionCircle.options.color = "#111111" + break; + } + } + if (this.getPosition() != this.#acquisitionCircle.getLatLng()) + this.#acquisitionCircle.setLatLng(this.getPosition()); + } + else { + if (getApp().getMap().hasLayer(this.#acquisitionCircle)) + this.#acquisitionCircle.removeFrom(getApp().getMap()); + } + + /* Engagement circles */ + var shortEngagementRangeCheck = (engagementRange > nmToM(3) || !getApp().getMap().getVisibilityOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]); + if (getApp().getMap().getVisibilityOptions()[SHOW_UNITS_ENGAGEMENT_RINGS] && 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()) { + case "red": + this.#engagementCircle.options.color = "#FF5858"; + break; + case "blue": + this.#engagementCircle.options.color = "#3BB9FF"; + break; + default: + this.#engagementCircle.options.color = "#CFD9E8" + break; + } + } + if (this.getPosition() != this.#engagementCircle.getLatLng()) + this.#engagementCircle.setLatLng(this.getPosition()); + } + else { + if (getApp().getMap().hasLayer(this.#engagementCircle)) + this.#engagementCircle.removeFrom(getApp().getMap()); + } + } + } + + #clearRanges() { + if (getApp().getMap().hasLayer(this.#acquisitionCircle)) + this.#acquisitionCircle.removeFrom(getApp().getMap()); + + if (getApp().getMap().hasLayer(this.#engagementCircle)) + this.#engagementCircle.removeFrom(getApp().getMap()); + } + + #drawTarget() { + if (this.#targetPosition.lat != 0 && this.#targetPosition.lng != 0 && getApp().getMap().getVisibilityOptions()[SHOW_UNIT_PATHS]) { + this.#drawTargetPosition(this.#targetPosition); + } + else if (this.#targetID != 0 && getApp().getMap().getVisibilityOptions()[SHOW_UNIT_TARGETS]) { + 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()); + } + } + else + this.#clearTargetPosition(); + } + + #drawTargetPosition(targetPosition: LatLng) { + if (!getApp().getMap().hasLayer(this.#targetPositionMarker)) + this.#targetPositionMarker.addTo(getApp().getMap()); + if (!getApp().getMap().hasLayer(this.#targetPositionPolyline)) + this.#targetPositionPolyline.addTo(getApp().getMap()); + this.#targetPositionMarker.setLatLng(new LatLng(targetPosition.lat, targetPosition.lng)); + + if (this.getState() === 'simulate-fire-fight' && this.getShotsScatter() != MAX_SHOTS_SCATTER) { + let turfUnitPosition = turf.point([this.getPosition().lng, this.getPosition().lat]); + let turfTargetPosition = turf.point([targetPosition.lng, targetPosition.lat]); + + let bearing = turf.bearing(turfUnitPosition, turfTargetPosition); + let scatterDistance = turf.distance(turfUnitPosition, turfTargetPosition) * Math.tan((MAX_SHOTS_SCATTER - this.getShotsScatter()) * deg2rad(SHOTS_SCATTER_DEGREES)); + let destination1 = turf.destination(turfTargetPosition, scatterDistance, bearing + 90); + let destination2 = turf.destination(turfTargetPosition, scatterDistance, bearing - 90); + + this.#targetPositionPolyline.setStyle({dashArray: "4, 8"}); + this.#targetPositionPolyline.setLatLngs([new LatLng(destination1.geometry.coordinates[1], destination1.geometry.coordinates[0]), new LatLng(this.#position.lat, this.#position.lng), new LatLng(destination2.geometry.coordinates[1], destination2.geometry.coordinates[0])]) + } else { + this.#targetPositionPolyline.setStyle({dashArray: ""}); + this.#targetPositionPolyline.setLatLngs([new LatLng(this.#position.lat, this.#position.lng), new LatLng(targetPosition.lat, targetPosition.lng)]) + } + } + + #clearTargetPosition() { + if (getApp().getMap().hasLayer(this.#targetPositionMarker)) + this.#targetPositionMarker.removeFrom(getApp().getMap()); + + if (getApp().getMap().hasLayer(this.#targetPositionPolyline)) + this.#targetPositionPolyline.removeFrom(getApp().getMap()); + } + + #onZoom(e: any) { + if (this.checkZoomRedraw()) + this.#redrawMarker(); + this.#updateMarker(); + } +} + +export abstract class AirUnit extends Unit { + getIconOptions() { + var belongsToCommandedCoalition = this.belongsToCommandedCoalition(); + return { + showState: belongsToCommandedCoalition, + showVvi: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showHealth: false, + showHotgroup: belongsToCommandedCoalition, + showUnitIcon: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value))), + showFuel: belongsToCommandedCoalition, + showAmmo: belongsToCommandedCoalition, + showSummary: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showCallsign: belongsToCommandedCoalition, + rotateToHeading: false + }; + } + + appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) { + if (targetUnit !== null) { + if (targetUnit != this) { + contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) }); + contextActionSet.addContextAction(this, "follow", "Follow unit", "Follow this unit in formation", (units: Unit[]) => { targetUnit.showFollowOptions(units); }, false); // Don't hide the context menu after the execution (to show the follow options) + } + if (targetUnit.getSelected()) { + contextActionSet.addContextAction(this, "refuel", "Refuel", "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB", (units: Unit[]) => { getApp().getUnitsManager().refuel(units) }); + } + if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) { + contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); }); + } + } + + if (targetPosition !== null) { + contextActionSet.addContextAction(this, "bomb", "Precision bombing", "Precision bombing of a specific point", (units: Unit[]) => { getApp().getUnitsManager().bombPoint(targetPosition, units) }); + contextActionSet.addContextAction(this, "carpet-bomb", "Carpet bombing", "Carpet bombing close to a point", (units: Unit[]) => { getApp().getUnitsManager().carpetBomb(targetPosition, units) }); + } + } +} + +export class Aircraft extends AirUnit { + constructor(ID: number) { + super(ID); + } + + getCategory() { + return "Aircraft"; + } + + appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) { + super.appendContextActions(contextActionSet, targetUnit, targetPosition); + + if (targetPosition === null && this.getSelected()) { + contextActionSet.addContextAction(this, "refuel", "Refuel", "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB", (units: Unit[]) => { getApp().getUnitsManager().refuel(units) }); + } + } + + getMarkerCategory() { + return "aircraft"; + } + + getDefaultMarker() { + return "aircraft"; + } +} + +export class Helicopter extends AirUnit { + constructor(ID: number) { + super(ID); + } + + getCategory() { + return "Helicopter"; + } + + appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) { + super.appendContextActions(contextActionSet, targetUnit, targetPosition); + + if (targetPosition !== null) + contextActionSet.addContextAction(this, "land-at-point", "Land here", "land at this precise location", (units: Unit[]) => { getApp().getUnitsManager().landAtPoint(targetPosition, units) }); + } + + getMarkerCategory() { + return "helicopter"; + } + + getDefaultMarker() { + return "helicopter"; + } +} + +export class GroundUnit extends Unit { + constructor(ID: number) { + super(ID); + } + + getIconOptions() { + var belongsToCommandedCoalition = this.belongsToCommandedCoalition(); + return { + showState: belongsToCommandedCoalition, + showVvi: false, + showHealth: true, + showHotgroup: belongsToCommandedCoalition, + showUnitIcon: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: this.getDatabaseEntry()?.type === "SAM Site", + showFuel: false, + showAmmo: false, + showSummary: false, + showCallsign: belongsToCommandedCoalition, + rotateToHeading: false + }; + } + + appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) { + contextActionSet.addContextAction(this, "group-ground", "Group ground units", "Create a group of ground units", (units: Unit[]) => { getApp().getUnitsManager().createGroup(units) }); + + if (targetUnit !== null) { + if (targetUnit != this) { + contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) }); + } + + if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) { + contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); }); + } + } + + if (targetPosition !== null) { + if (this.canTargetPoint()) { + contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Fire at a specific area on the ground", (units: Unit[]) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) }); + contextActionSet.addContextAction(this, "simulate-fire-fight", "Simulate fire fight", "Simulate a fire fight by shooting randomly in a certain large area.\nWARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().simulateFireFight(targetPosition, units) }); + } + } + else { + if (this.canAAA()) { + contextActionSet.addContextAction(this, "scenic-aaa", "Scenic AAA", "Shoot AAA in the air without aiming at any target, when an enemy unit gets close enough.\nWARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().scenicAAA(units) }, undefined, { + "isScenic": true + }); + contextActionSet.addContextAction(this, "miss-aaa", "Dynamic accuracy AAA", "Shoot AAA towards the closest enemy unit, but don't aim precisely.\nWARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().missOnPurpose(units) }, undefined, { + "isScenic": true + }); + } + } + } + + getCategory() { + return "GroundUnit"; + } + + getType() { + var blueprint = groundUnitDatabase.getByName(this.getName()); + return blueprint?.type ? blueprint.type : ""; + } + + /* 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().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && 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) + return unit + return prev; + }, null); + unitWhenGrouped = (member !== null ? member?.getDatabaseEntry()?.unitWhenGrouped : unitWhenGrouped); + } + if (unitWhenGrouped) + return this.getDatabase()?.getByName(unitWhenGrouped) ?? this.getDatabase()?.getUnkownUnit(this.getName()); + else + return this.getDatabase()?.getByName(this.getName()) ?? this.getDatabase()?.getUnkownUnit(this.getName()); + } + + /* 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().getVisibilityOptions()[HIDE_GROUP_MEMBERS] 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)) + } + + getMarkerCategory() { + if (/\bAAA|SAM\b/.test(this.getType()) || /\bmanpad|stinger\b/i.test(this.getType())) + return "groundunit-sam"; + else + return "groundunit"; + } + + getDefaultMarker() { + return "groundunit"; + } +} + +export class NavyUnit extends Unit { + constructor(ID: number) { + super(ID); + } + + getIconOptions() { + var belongsToCommandedCoalition = this.belongsToCommandedCoalition(); + return { + showState: belongsToCommandedCoalition, + showVvi: false, + showHealth: true, + showHotgroup: true, + showUnitIcon: (belongsToCommandedCoalition || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: false, + showCallsign: belongsToCommandedCoalition, + rotateToHeading: false + }; + } + + appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) { + contextActionSet.addContextAction(this, "group-navy", "Group navy units", "Create a group of navy units", (units: Unit[]) => { getApp().getUnitsManager().createGroup(units) }); + + if (targetUnit !== null) { + if (targetUnit != this) { + contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) }); + } + if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) { + contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); }); + } + } + + if (targetPosition !== null) { + contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Fire at a specific area on the ground", (units: Unit[]) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) }); + } + } + + getCategory() { + return "NavyUnit"; + } + + getType() { + var blueprint = navyUnitDatabase.getByName(this.getName()); + return blueprint?.type ? blueprint.type : ""; + } + + getMarkerCategory() { + return "navyunit"; + } + + getDefaultMarker() { + return "navyunit"; + } +} diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts new file mode 100644 index 00000000..b908cfec --- /dev/null +++ b/frontend/react/src/unit/unitsmanager.ts @@ -0,0 +1,1544 @@ +import { LatLng, LatLngBounds } from "leaflet"; +import { getApp } from "../olympusapp"; +import { Unit } from "./unit"; +import { bearingAndDistanceToLatLng, deg2rad, getGroundElevation, getUnitDatabaseByCategory, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils"; +import { CoalitionArea } from "../map/coalitionarea/coalitionarea"; +import { groundUnitDatabase } from "./databases/groundunitdatabase"; +import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT } from "../constants/constants"; +import { DataExtractor } from "../server/dataextractor"; +import { citiesDatabase } from "./databases/citiesdatabase"; +import { aircraftDatabase } from "./databases/aircraftdatabase"; +import { helicopterDatabase } from "./databases/helicopterdatabase"; +import { navyUnitDatabase } from "./databases/navyunitdatabase"; +import { TemporaryUnitMarker } from "../map/markers/temporaryunitmarker"; +//import { Popup } from "../popups/popup"; +//import { HotgroupPanel } from "../panels/hotgrouppanel"; +import { Contact, UnitBlueprint, UnitData, UnitSpawnTable } from "../interfaces"; +//import { Dialog } from "../dialog/dialog"; +import { Group } from "./group"; +import { UnitDataFileExport } from "./importexport/unitdatafileexport"; +import { UnitDataFileImport } from "./importexport/unitdatafileimport"; + +/** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only + * result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows + * to avoid client/server and client/client inconsistencies. + */ +export class UnitsManager { + #copiedUnits: UnitData[]; + #deselectionEventDisabled: boolean = false; + #requestDetectionUpdate: boolean = false; + #selectionEventDisabled: boolean = false; + //#slowDeleteDialog!: Dialog; + #units: { [ID: number]: Unit }; + #groups: { [groupName: string]: Group } = {}; + #unitDataExport!: UnitDataFileExport; + #unitDataImport!: UnitDataFileImport; + + constructor() { + this.#copiedUnits = []; + this.#units = {}; + + document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) }); + document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true }); + document.addEventListener('copy', () => this.copy()); + document.addEventListener('deleteSelectedUnits', () => this.delete()); + document.addEventListener('explodeSelectedUnits', (e: any) => this.delete(true, e.detail.type)); + document.addEventListener('exportToFile', () => this.exportToFile()); + document.addEventListener('importFromFile', () => this.importFromFile()); + document.addEventListener('keyup', (event) => this.#onKeyUp(event)); + document.addEventListener('paste', () => this.paste()); + document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.changeAltitude(e.detail.type) }); + document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.changeSpeed(e.detail.type) }); + document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail)); + document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail)); + document.addEventListener("toggleMarkerProtection", (ev: CustomEventInit) => { this.#showNumberOfSelectedProtectedUnits() }); + + //this.#slowDeleteDialog = new Dialog("slow-delete-dialog"); + } + + /** + * + * @returns All the existing units, both alive and dead + */ + getUnits() { + return this.#units; + } + + /** Get a specific unit by ID + * + * @param ID ID of the unit. The ID shall be the same as the unit ID in DCS. + * @returns Unit object, or null if no unit with said ID exists. + */ + getUnitByID(ID: number) { + if (ID in this.#units) + return this.#units[ID]; + else + return null; + } + + /** Returns all the units that belong to a hotgroup + * + * @param hotgroup Hotgroup number + * @returns Array of units that belong to hotgroup + */ + getUnitsByHotgroup(hotgroup: number) { + return Object.values(this.#units).filter((unit: Unit) => { return unit.getAlive() && unit.getHotgroup() == hotgroup }); + } + + /** Add a new unit to the manager + * + * @param ID ID of the new unit + * @param category Either "Aircraft", "Helicopter", "GroundUnit", or "NavyUnit". Determines what class will be used to create the new unit accordingly. + */ + addUnit(ID: number, category: string) { + if (category) { + /* Get the constructor from the unit category */ + var constructor = Unit.getConstructor(category); + if (constructor != undefined) { + this.#units[ID] = new constructor(ID); + } + } + } + + /** Sort units segregated groups based on controlling type and protection, if DCS-controlled + * + * @param units + * @returns Object + */ + segregateUnits(units: Unit[]): { [key: string]: [] } { + const data: any = { + controllable: [], + dcsProtected: [], + dcsUnprotected: [], + human: [], + olympus: [] + }; + const map = getApp().getMap(); + + units.forEach(unit => { + if (unit.getHuman()) + data.human.push(unit); + else if (unit.isControlledByOlympus()) + data.olympus.push(unit); + else if (map.getIsUnitProtected(unit)) + data.dcsProtected.push(unit); + else + data.dcsUnprotected.push(unit); + }); + data.controllable = [].concat(data.dcsUnprotected, data.human, data.olympus); + return data; + } + + /** + * + * @param numOfProtectedUnits number + */ + showProtectedUnitsPopup(numOfProtectedUnits: number) { + if (numOfProtectedUnits < 1) + return; + const messageText = (numOfProtectedUnits === 1) ? `Unit is protected` : `All selected units are protected`; + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(messageText); + // Cheap way for now until we use more locks + let lock = document.querySelector("#unit-visibility-control button.lock"); + lock.classList.add("prompt"); + setTimeout(() => lock.classList.remove("prompt"), 4000); + } + + /** Update the data of all the units. The data is directly decoded from the binary buffer received from the REST Server. This is necessary for performance and bandwidth reasons. + * + * @param buffer The arraybuffer, encoded according to the ICD defined in: TODO Add reference to ICD + * @returns The decoded updateTime of the data update. + */ + update(buffer: ArrayBuffer) { + /* Extract the data from the arraybuffer. Since data is encoded dynamically (not all data is always present, but rather only the data that was actually updated since the last request). + No a prori casting can be performed. On the contrary, the array is decoded incrementally, depending on the DataIndexes of the data. The actual data decoding is performed by the Unit class directly. + Every time a piece of data is decoded the decoder seeker is incremented. */ + var dataExtractor = new DataExtractor(buffer); + + var updateTime = Number(dataExtractor.extractUInt64()); + + /* Run until all data is extracted or an error occurs */ + while (dataExtractor.getSeekPosition() < buffer.byteLength) { + /* Extract the unit ID */ + const ID = dataExtractor.extractUInt32(); + + /* If the ID of the unit does not yet exist, create the unit, if the category is known. If it isn't, some data must have been lost and we need to wait for another update */ + if (!(ID in this.#units)) { + const datumIndex = dataExtractor.extractUInt8(); + if (datumIndex == DataIndexes.category) { + const category = dataExtractor.extractString(); + this.addUnit(ID, category); + } + else { + /* Inconsistent data, we need to wait for a refresh */ + return updateTime; + } + } + /* Update the data of the unit */ + this.#units[ID]?.setData(dataExtractor); + } + + /* Update the unit groups */ + for (let ID in this.#units) { + const unit = this.#units[ID]; + const groupName = unit.getGroupName(); + + if (groupName !== "") { + /* If the group does not yet exist, create it */ + if (!(groupName in this.#groups)) + this.#groups[groupName] = new Group(groupName); + + /* If the unit was not assigned to a group yet, assign it */ + if (unit.getGroup() === null) + this.#groups[groupName].addMember(unit); + } + } + + /* If we are not in Game Master mode, visibility of units by the user is determined by the detections of the units themselves. This is performed here. + This operation is computationally expensive, therefore it is only performed when #requestDetectionUpdate is true. This happens whenever a change in the detectionUpdates is detected + */ + if (this.#requestDetectionUpdate && getApp().getMissionManager().getCommandModeOptions().commandMode != GAME_MASTER) { + /* Create a dictionary of empty detection methods arrays */ + var detectionMethods: { [key: string]: number[] } = {}; + for (let ID in this.#units) + detectionMethods[ID] = []; + for (let ID in getApp().getWeaponsManager().getWeapons()) + detectionMethods[ID] = []; + + /* Fill the array with the detection methods */ + for (let ID in this.#units) { + const unit = this.#units[ID]; + if (unit.getAlive() && unit.belongsToCommandedCoalition()) { + const contacts = unit.getContacts(); + contacts.forEach((contact: Contact) => { + const contactID = contact.ID; + if (contactID in detectionMethods && !(detectionMethods[contactID].includes(contact.detectionMethod))) + detectionMethods[contactID]?.push(contact.detectionMethod); + }) + } + } + + /* Set the detection methods for every unit */ + for (let ID in this.#units) { + const unit = this.#units[ID]; + unit?.setDetectionMethods(detectionMethods[ID]); + } + + /* Set the detection methods for every weapon (weapons must be detected too) */ + for (let ID in getApp().getWeaponsManager().getWeapons()) { + const weapon = getApp().getWeaponsManager().getWeaponByID(parseInt(ID)); + weapon?.setDetectionMethods(detectionMethods[ID]); + } + + this.#requestDetectionUpdate = false; + } + + /* Update the detection lines of all the units. This code is handled by the UnitsManager since it must be run both when the detected OR the detecting unit is updated */ + for (let ID in this.#units) { + if (this.#units[ID].getSelected()) + this.#units[ID].drawLines(); + }; + + return updateTime; + } + + /** Set a unit as "selected", which will allow to perform operations on it, like giving it a destination, setting it to attack a target, and so on + * + * @param ID The ID of the unit to select + * @param deselectAllUnits If true, the unit will be the only selected unit + */ + selectUnit(ID: number, deselectAllUnits: boolean = true) { + if (deselectAllUnits) + this.getSelectedUnits().filter((unit: Unit) => unit.ID !== ID).forEach((unit: Unit) => unit.setSelected(false)); + this.#units[ID]?.setSelected(true); + } + + /** Select all visible units inside a bounding rectangle + * + * @param bounds Leaflet bounds object defining the selection area + */ + selectFromBounds(bounds: LatLngBounds) { + this.deselectAllUnits(); + for (let ID in this.#units) { + if (this.#units[ID].getHidden() == false) { + var latlng = new LatLng(this.#units[ID].getPosition().lat, this.#units[ID].getPosition().lng); + if (bounds.contains(latlng)) { + this.#units[ID].setSelected(true); + } + } + } + } + + /** Select units by hotgroup. A hotgroup can be created to quickly select multiple units using keyboard bindings + * + * @param hotgroup The hotgroup number + */ + selectUnitsByHotgroup(hotgroup: number, deselectAllUnits: boolean = true) { + + if (deselectAllUnits) { + this.deselectAllUnits(); + } + + this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setSelected(true)) + } + + /** Get all the currently selected units + * + * @param options Selection options + * @returns Array of selected units + */ + getSelectedUnits(options?: { excludeHumans?: boolean, excludeProtected?: boolean, onlyOnePerGroup?: boolean, showProtectionReminder?: boolean }) { + let selectedUnits: Unit[] = []; + let numProtectedUnits = 0; + for (const [ID, unit] of Object.entries(this.#units)) { + if (unit.getSelected()) { + if (options) { + if (options.excludeHumans && unit.getHuman()) + continue + + if (options.excludeProtected === true && this.#unitIsProtected(unit)) { + numProtectedUnits++; + continue; + } + } + selectedUnits.push(unit); + } + } + if (options) { + if (options.showProtectionReminder === true && numProtectedUnits > selectedUnits.length && selectedUnits.length === 0) + this.showProtectedUnitsPopup(numProtectedUnits); + + if (options.onlyOnePerGroup) { + var temp: Unit[] = []; + for (let unit of selectedUnits) { + if (!temp.some((otherUnit: Unit) => unit.getGroupName() == otherUnit.getGroupName())) + temp.push(unit); + } + selectedUnits = temp; + } + } + return selectedUnits; + } + + /** Deselects all currently selected units + * + */ + deselectAllUnits() { + for (let ID in this.#units) { + this.#units[ID].setSelected(false); + } + } + + /** Deselect a specific unit + * + * @param ID ID of the unit to deselect + */ + deselectUnit(ID: number) { + this.#units[ID]?.setSelected(false); + } + + /** This function allows to quickly determine the categories (Aircraft, Helicopter, GroundUnit, NavyUnit) of an array units. This allows to enable/disable specific controls which can only be applied + * to specific categories. + * + * @param units Array of units of which to retrieve the categories + * @returns Array of categories. Each category is present only once. + */ + getUnitsCategories(units: Unit[]) { + if (units.length == 0) + return []; + return units.map((unit: Unit) => { + return unit.getCategory(); + })?.filter((value: any, index: number, array: string[]) => { + return array.indexOf(value) === index; + }); + } + + /** This function returns the value of a variable for each of the units in the input array. If all the units have the same value, returns the value, else returns undefined. This function is useful to + * present units data in the control panel, which will print a specific value only if it is the same for all the units. If the values are different, the control panel will show a "mixed values" value, or similar. + * + * @param variableGetter CallableFunction that returns the requested variable. Example: getUnitsVariable((unit: Unit) => unit.getName(), foo) will return a string value if all the units have the same name, otherwise it will return undefined. + * @param units Array of units of which to retrieve the variable + * @returns The value of the variable if all units have the same value, else undefined + */ + getUnitsVariable(variableGetter: CallableFunction, units: Unit[]) { + if (units.length == 0) + return undefined; + + var value: any = variableGetter(units[0]); + units.forEach((unit: Unit) => { + if (variableGetter(unit) !== value) + value = undefined; + }); + return value; + }; + + /** For a given unit, it returns if and how it is being detected by other units. NOTE: this function will return how a unit is being detected, i.e. how other units are detecting it. It will not return + * what the unit is detecting. + * + * @param unit The unit of which to retrieve the "detected" methods. + * @returns Array of detection methods + */ + getUnitDetectedMethods(unit: Unit) { + var detectionMethods: number[] = []; + for (let idx in this.#units) { + if (this.#units[idx].getAlive() && this.#units[idx].getIsLeader() && this.#units[idx].getCoalition() !== "neutral" && this.#units[idx].getCoalition() != unit.getCoalition()) { + this.#units[idx].getContacts().forEach((contact: Contact) => { + if (contact.ID == unit.ID && !detectionMethods.includes(contact.detectionMethod)) + detectionMethods.push(contact.detectionMethod); + }); + } + } + return detectionMethods; + } + + /*********************** Unit actions on selected units ************************/ + /** Give a new destination to the selected units + * + * @param latlng Position of the new destination + * @param mantainRelativePosition If true, the selected units will mantain their relative positions when reaching the target. This is useful to maintain a formation for groun/navy units + * @param rotation Rotation in radians by which the formation will be rigidly rotated. E.g. a ( V ) formation will look like this ( < ) if rotated pi/4 radians (90 degrees) + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + addDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + /* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative positions */ + var unitDestinations: { [key: number]: LatLng } = {}; + if (mantainRelativePosition) + unitDestinations = this.computeGroupDestination(latlng, rotation); + else + units.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng; }); + + units.forEach((unit: Unit) => { + /* If a unit is following another unit, and that unit is also selected, send the command to the followed ("leader") unit */ + if (unit.getState() === "follow") { + const leader = this.getUnitByID(unit.getLeaderID()) + if (leader && leader.getSelected()) + leader.addDestination(latlng); + else + unit.addDestination(latlng); + } + else { + if (unit.ID in unitDestinations) + unit.addDestination(unitDestinations[unit.ID]); + } + }); + this.#showActionMessage(units, " new destination added"); + } + + /** Clear the destinations of all the selected units + * + */ + clearDestinations(units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: false }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + for (let idx in units) { + const unit = units[idx]; + if (unit.getState() === "follow") { + const leader = this.getUnitByID(unit.getLeaderID()) + if (leader && leader.getSelected()) + leader.clearDestinations(); + else + unit.clearDestinations(); + } + else + unit.clearDestinations(); + } + } + + /** Instruct all the selected units to land at a specific location + * + * @param latlng Location where to land at + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + landAt(latlng: LatLng, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.landAt(latlng)); + + this.#showActionMessage(units, " landing"); + } + + /** Instruct all the selected units to change their speed + * + * @param speedChange Speed change, either "stop", "slow", or "fast". The specific value depends on the unit category + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + changeSpeed(speedChange: string, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.changeSpeed(speedChange)); + } + + /** Instruct all the selected units to change their altitude + * + * @param altitudeChange Altitude change, either "climb" or "descend". The specific value depends on the unit category + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + changeAltitude(altitudeChange: string, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.changeAltitude(altitudeChange)); + } + + /** Set a specific speed to all the selected units + * + * @param speed Value to set, in m/s + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setSpeed(speed: number, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setSpeed(speed)); + this.#showActionMessage(units, `setting speed to ${msToKnots(speed)} kts`); + } + + /** Set a specific speed type to all the selected units + * + * @param speedType Value to set, either "CAS" or "GS". If "CAS" is selected, the unit will try to maintain the selected Calibrated Air Speed, but DCS will still only maintain a Ground Speed value so errors may arise depending on wind. + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setSpeedType(speedType: string, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setSpeedType(speedType)); + this.#showActionMessage(units, `setting speed type to ${speedType}`); + } + + /** Set a specific altitude to all the selected units + * + * @param altitude Value to set, in m + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setAltitude(altitude: number, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setAltitude(altitude)); + this.#showActionMessage(units, `setting altitude to ${mToFt(altitude)} ft`); + } + + /** Set a specific altitude type to all the selected units + * + * @param altitudeType Value to set, either "ASL" or "AGL". If "AGL" is selected, the unit will try to maintain the selected Above Ground Level altitude. Due to a DCS bug, this will only be true at the final position. + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setAltitudeType(altitudeType: string, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setAltitudeType(altitudeType)); + this.#showActionMessage(units, `setting altitude type to ${altitudeType}`); + } + + /** Set a specific ROE to all the selected units + * + * @param ROE Value to set, see constants for acceptable values + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setROE(ROE: string, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setROE(ROE)); + this.#showActionMessage(units, `ROE set to ${ROE}`); + } + + /** Set a specific reaction to threat to all the selected units + * + * @param reactionToThreat Value to set, see constants for acceptable values + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setReactionToThreat(reactionToThreat: string, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setReactionToThreat(reactionToThreat)); + this.#showActionMessage(units, `reaction to threat set to ${reactionToThreat}`); + } + + /** Set a specific emissions & countermeasures to all the selected units + * + * @param emissionCountermeasure Value to set, see constants for acceptable values + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setEmissionsCountermeasures(emissionCountermeasure: string, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setEmissionsCountermeasures(emissionCountermeasure)); + this.#showActionMessage(units, `emissions & countermeasures set to ${emissionCountermeasure}`); + } + + /** Turn selected units on or off, only works on ground and navy units + * + * @param onOff If true, the unit will be turned on + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setOnOff(onOff: boolean, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setOnOff(onOff)); + this.#showActionMessage(units, `unit active set to ${onOff}`); + } + + /** Instruct the selected units to follow roads, only works on ground units + * + * @param followRoads If true, units will follow roads + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setFollowRoads(followRoads: boolean, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setFollowRoads(followRoads)); + this.#showActionMessage(units, `follow roads set to ${followRoads}`); + } + + /** Instruct selected units to operate as a certain coalition + * + * @param operateAsBool If true, units will operate as blue + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setOperateAs(operateAsBool: boolean, units: Unit[] | null = null) { + var operateAs = operateAsBool ? "blue" : "red"; + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setOperateAs(operateAs)); + this.#showActionMessage(units, `operate as set to ${operateAs}`); + } + + /** Instruct units to attack a specific unit + * + * @param ID ID of the unit to attack + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + attackUnit(ID: number, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.attackUnit(ID)); + this.#showActionMessage(units, `attacking unit ${this.getUnitByID(ID)?.getUnitName()}`); + } + + /** Instruct units to refuel at the nearest tanker, if possible. Else units will RTB + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + refuel(units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + segregatedUnits.controllable.forEach((unit: Unit) => unit.refuel()); + this.#showActionMessage(segregatedUnits.controllable, `sent to nearest tanker`); + } + + /** Instruct the selected units to follow another unit in a formation. Only works for aircrafts and helicopters. + * + * @param ID ID of the unit to follow + * @param offset Optional parameter, defines a static offset. X: front-rear, positive front, Y: top-bottom, positive top, Z: left-right, positive right + * @param formation Optional parameter, defines a predefined formation type. Values are: "trail", "echelon-lh", "echelon-rh", "line-abreast-lh", "line-abreast-rh", "front", "diamond" + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + followUnit(ID: number, offset?: { "x": number, "y": number, "z": number }, formation?: string, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + if (offset == undefined) { + /* Simple formations with fixed offsets */ + offset = { "x": 0, "y": 0, "z": 0 }; + if (formation === "trail") { offset.x = -50; offset.y = -30; offset.z = 0; } + else if (formation === "echelon-lh") { offset.x = -50; offset.y = -10; offset.z = -50; } + else if (formation === "echelon-rh") { offset.x = -50; offset.y = -10; offset.z = 50; } + else if (formation === "line-abreast-lh") { offset.x = 0; offset.y = 0; offset.z = -50; } + else if (formation === "line-abreast-rh") { offset.x = 0; offset.y = 0; offset.z = 50; } + else if (formation === "front") { offset.x = 100; offset.y = 0; offset.z = 0; } + else offset = undefined; + } + + var count = 1; + var xr = 0; var yr = 1; var zr = -1; + var layer = 1; + units.forEach((unit: Unit) => { + if (unit.ID !== ID) { + if (offset != undefined) + /* Offset is set, apply it */ + unit.followUnit(ID, { "x": offset.x * count, "y": offset.y * count, "z": offset.z * count }); + else { + /* More complex formations with variable offsets */ + if (formation === "diamond") { + var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4); + var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4); + unit.followUnit(ID, { "x": -yl * 50, "y": zr * 10, "z": xl * 50 }); + + if (yr == 0) { layer++; xr = 0; yr = layer; zr = -layer; } + else { + if (xr < layer) { xr++; zr--; } + else { yr--; zr++; } + } + } + } + count++; + } + }) + this.#showActionMessage(units, `following unit ${this.getUnitByID(ID)?.getUnitName()}`); + } + + /** Instruct the selected units to perform precision bombing of specific coordinates + * + * @param latlng Location to bomb + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + bombPoint(latlng: LatLng, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.bombPoint(latlng)); + this.#showActionMessage(units, `unit bombing point`); + } + + /** Instruct the selected units to perform carpet bombing of specific coordinates + * + * @param latlng Location to bomb + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + carpetBomb(latlng: LatLng, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.carpetBomb(latlng)); + this.#showActionMessage(units, `unit carpet bombing point`); + } + + /** Instruct the selected units to fire at specific coordinates + * + * @param latlng Location to fire at + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + fireAtArea(latlng: LatLng, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.fireAtArea(latlng)); + this.#showActionMessage(units, `unit firing at area`); + } + + /** Instruct the selected units to simulate a fire fight at specific coordinates + * + * @param latlng Location to fire at + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + simulateFireFight(latlng: LatLng, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + getGroundElevation(latlng, (response: string) => { + var groundElevation: number | null = null; + try { + groundElevation = parseFloat(response); + } catch { + console.warn("Simulate fire fight: could not retrieve ground elevation") + } + units?.forEach((unit: Unit) => unit.simulateFireFight(latlng, groundElevation)); + }); + this.#showActionMessage(units, `unit simulating fire fight`); + } + + /** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + scenicAAA(units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.scenicAAA()); + this.#showActionMessage(units, `unit set to perform scenic AAA`); + } + + /** Instruct units to enter into dynamic accuracy/miss on purpose mode. Units will aim to the nearest enemy unit but not precisely. + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + missOnPurpose(units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.missOnPurpose()); + this.#showActionMessage(units, `unit set to perform miss-on-purpose AAA`); + } + + /** Instruct units to land at specific point + * + * @param latlng Point where to land + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + landAtPoint(latlng: LatLng, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.landAtPoint(latlng)); + this.#showActionMessage(units, `unit landing at point`); + } + + /** Set a specific shots scatter to all the selected units + * + * @param shotsScatter Value to set + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setShotsScatter(shotsScatter: number, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setShotsScatter(shotsScatter)); + this.#showActionMessage(units, `shots scatter set to ${shotsScatter}`); + } + + /** Set a specific shots intensity to all the selected units + * + * @param shotsScatter Value to set + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setShotsIntensity(shotsIntensity: number, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + units.forEach((unit: Unit) => unit.setShotsIntensity(shotsIntensity)); + this.#showActionMessage(units, `shots intensity set to ${shotsIntensity}`); + } + + /*********************** Control operations on selected units ************************/ + /** See getUnitsCategories for more info + * + * @returns Category array of the selected units. + */ + getSelectedUnitsCategories() { + return this.getUnitsCategories(this.getSelectedUnits()); + }; + + /** See getUnitsVariable for more info + * + * @param variableGetter CallableFunction that returns the requested variable. Example: getUnitsVariable((unit: Unit) => unit.getName(), foo) will return a string value if all the units have the same name, otherwise it will return undefined. + * @returns The value of the variable if all units have the same value, else undefined + */ + getSelectedUnitsVariable(variableGetter: CallableFunction) { + return this.getUnitsVariable(variableGetter, this.getSelectedUnits()); + }; + + /** Groups the selected units in a single (DCS) group, if all the units have the same category + * + */ + createGroup(units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: false, showProtectionReminder: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + if (this.getUnitsCategories(units).length == 1) { + var unitsData: { ID: number, location: LatLng }[] = []; + units.forEach((unit: Unit) => unitsData.push({ ID: unit.ID, location: unit.getPosition() })); + getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */); + this.#showActionMessage(units, `created a group`); + } else { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Groups can only be created from units of the same category`); + } + } + + /** Set the hotgroup for the selected units. It will be the only hotgroup of the unit + * + * @param hotgroup Hotgroup number + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + setHotgroup(hotgroup: number, units: Unit[] | null = null) { + this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setHotgroup(null)); + this.addToHotgroup(hotgroup); + } + + /** Add the selected units to a hotgroup. Units can be in multiple hotgroups at the same type + * + * @param hotgroup Hotgroup number + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + */ + addToHotgroup(hotgroup: number, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits(); + units.forEach((unit: Unit) => unit.setHotgroup(hotgroup)); + this.#showActionMessage(units, `added to hotgroup ${hotgroup}`); + //(getApp().getPanelsManager().get("hotgroup") as HotgroupPanel).refreshHotgroups(); + } + + /** Delete the selected units + * + * @param explosion If true, the unit will be deleted using an explosion + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + * @returns + */ + delete(explosion: boolean = false, explosionType: string = "", units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeProtected: true, showProtectionReminder: true }); /* Can be applied to humans too */ + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return; + } + + units = segregatedUnits.controllable; + + const selectionContainsAHuman = units.some((unit: Unit) => { + return unit.getHuman() === true; + }); + + if (selectionContainsAHuman && !confirm("Your selection includes a human player. Deleting humans causes their vehicle to crash.\n\nAre you sure you want to do this?")) { + return; + } + + const doDelete = (explosion = false, explosionType = "", immediate = false) => { + units?.forEach((unit: Unit) => unit.delete(explosion, explosionType, immediate)); + this.#showActionMessage(units as Unit[], `deleted`); + } + + //if (units.length >= DELETE_SLOW_THRESHOLD) + // //this.#showSlowDeleteDialog(units).then((action: any) => { + // // if (action === "delete-slow") + // // doDelete(explosion, explosionType, false); + // // else if (action === "delete-immediate") + // // doDelete(explosion, explosionType, true); + // //}) + //else + doDelete(explosion, explosionType); + + } + + /** Compute the destinations of every unit in the selected units. This function preserves the relative positions of the units, and rotates the whole formation by rotation. + * + * @param latlng Center of the group after the translation + * @param rotation Rotation of the group, in radians + * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. + * @returns Array of positions for each unit, in order + */ + computeGroupDestination(latlng: LatLng, rotation: number, units: Unit[] | null = null) { + if (units === null) + units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true }); + + const segregatedUnits = this.segregateUnits(units); + if (segregatedUnits.controllable.length === 0) { + this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); + return {}; + } + + units = segregatedUnits.controllable; + + if (units.length === 0) + return {}; + + /* Compute the center of the group */ + var len = units.length; + var center = { x: 0, y: 0 }; + units.forEach((unit: Unit) => { + var mercator = latLngToMercator(unit.getPosition().lat, unit.getPosition().lng); + center.x += mercator.x / len; + center.y += mercator.y / len; + }); + + /* Compute the distances from the center of the group */ + var unitDestinations: { [key: number]: LatLng } = {}; + units.forEach((unit: Unit) => { + var mercator = latLngToMercator(unit.getPosition().lat, unit.getPosition().lng); + var distancesFromCenter = { dx: mercator.x - center.x, dy: mercator.y - center.y }; + + /* Rotate the distance according to the group rotation */ + var rotatedDistancesFromCenter: { dx: number, dy: number } = { dx: 0, dy: 0 }; + rotatedDistancesFromCenter.dx = distancesFromCenter.dx * Math.cos(deg2rad(rotation)) - distancesFromCenter.dy * Math.sin(deg2rad(rotation)); + rotatedDistancesFromCenter.dy = distancesFromCenter.dx * Math.sin(deg2rad(rotation)) + distancesFromCenter.dy * Math.cos(deg2rad(rotation)); + + /* Compute the final position of the unit */ + var destMercator = latLngToMercator(latlng.lat, latlng.lng); // Convert destination point to mercator + var unitMercator = { x: destMercator.x + rotatedDistancesFromCenter.dx, y: destMercator.y + rotatedDistancesFromCenter.dy }; // Compute final position of this unit in mercator coordinates + var unitLatLng = mercatorToLatLng(unitMercator.x, unitMercator.y); + unitDestinations[unit.ID] = new LatLng(unitLatLng.lat, unitLatLng.lng); + }); + + return unitDestinations; + } + + /** Copy the selected units and store their properties in memory + * + */ + copy(units: Unit[] | null = null) { + //if (!getApp().getContextManager().getCurrentContext().getAllowUnitCopying()) + // return; + + if (units === null) + units = this.getSelectedUnits(); + + if (units.length === 0) + return; + + /* A JSON is used to deepcopy the units, creating a "snapshot" of their properties at the time of the copy */ + this.#copiedUnits = JSON.parse(JSON.stringify(units.map((unit: Unit) => { return unit.getData() }))); /* Can be applied to humans too */ + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${this.#copiedUnits.length} units copied`); + } + + /*********************** Unit manipulation functions ************************/ + /** Paste the copied units + * + * @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 */ + if (getApp().getMissionManager().getCommandModeOptions().commandMode != GAME_MASTER) { + if (getApp().getMissionManager().getCommandModeOptions().restrictSpawns && getApp().getMissionManager().getRemainingSetupTime() < 0) { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Units can be pasted only during SETUP phase`); + return false; + } + + this.#copiedUnits.forEach((unit: UnitData) => { + let unitSpawnPoints = getUnitDatabaseByCategory(unit.category)?.getSpawnPointsByName(unit.name); + if (unitSpawnPoints !== undefined) + spawnPoints += unitSpawnPoints; + }) + + if (spawnPoints > getApp().getMissionManager().getAvailableSpawnPoints()) { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Not enough spawn points available!"); + return false; + } + } + + if (this.#copiedUnits.length > 0) { + /* Compute the position of the center of the copied units */ + var nUnits = this.#copiedUnits.length; + var avgLat = 0; + var avgLng = 0; + for (let idx in this.#copiedUnits) { + var unit = this.#copiedUnits[idx]; + avgLat += unit.position.lat / nUnits; + avgLng += unit.position.lng / nUnits; + } + + /* Organize the copied units in groups */ + var groups: { [key: string]: UnitData[] } = {}; + this.#copiedUnits.forEach((unit: UnitData) => { + if (!(unit.groupName in groups)) + groups[unit.groupName] = []; + groups[unit.groupName].push(unit); + }); + + /* Clone the units in groups */ + for (let groupName in groups) { + var units: { ID: number, location: LatLng }[] = []; + let markers: TemporaryUnitMarker[] = []; + groups[groupName].forEach((unit: UnitData) => { + var position = new LatLng(getApp().getMap().getMouseCoordinates().lat + unit.position.lat - avgLat, getApp().getMap().getMouseCoordinates().lng + unit.position.lng - avgLng); + markers.push(getApp().getMap().addTemporaryMarker(position, unit.name, unit.coalition)); + units.push({ ID: unit.ID, location: position }); + }); + + getApp().getServerManager().cloneUnits(units, false, spawnPoints, (res: any) => { + if (res.commandHash !== undefined) { + markers.forEach((marker: TemporaryUnitMarker) => { + marker.setCommandHash(res.commandHash); + }) + } + }); + } + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${this.#copiedUnits.length} units pasted`); + } + else { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("No units copied!"); + } + } + + /** Automatically create an Integrated Air Defence System from a CoalitionArea object. The units will be mostly focused around big cities. The bigger the city, the larger the amount of units created next to it. + * If the CoalitionArea does not contain any city, no units will be created + * + * @param coalitionArea Boundaries of the IADS + * @param types Array of unit types to add to the IADS, e.g. AAA, SAM, flak, MANPADS + * @param eras Array of eras to which the units added to the IADS can belong + * @param ranges Array of weapon ranges the units can have + * @param density Value between 0 and 100, controls the amout of units created + * @param distribution Value between 0 and 100, controls how "scattered" the units will be + */ + createIADS(coalitionArea: CoalitionArea, types: { [key: string]: boolean }, eras: { [key: string]: boolean }, ranges: { [key: string]: boolean }, density: number, distribution: number, forceCoalition: boolean) { + const activeTypes = Object.keys(types).filter((key: string) => { return types[key]; }); + const activeEras = Object.keys(eras).filter((key: string) => { return eras[key]; }); + const activeRanges = Object.keys(ranges).filter((key: string) => { return ranges[key]; }); + + var airbases = getApp().getMissionManager().getAirbases(); + Object.keys(airbases).forEach((airbaseName: string) => { + var airbase = airbases[airbaseName]; + /* Check if the city is inside the coalition area */ + if (polyContains(new LatLng(airbase.getLatLng().lat, airbase.getLatLng().lng), coalitionArea)) { + /* Arbitrary formula to obtain a number of units */ + var pointsNumber = 2 + 10 * density / 100; + for (let i = 0; i < pointsNumber; i++) { + /* Place the unit nearby the airbase, depending on the distribution parameter */ + var bearing = Math.random() * 360; + var distance = Math.random() * distribution * 100; + const latlng = bearingAndDistanceToLatLng(airbase.getLatLng().lat, airbase.getLatLng().lng, bearing, distance); + + /* Make sure the unit is still inside the coalition area */ + if (polyContains(latlng, coalitionArea)) { + const type = activeTypes[Math.floor(Math.random() * activeTypes.length)]; + if (Math.random() < IADSDensities[type]) { + /* Get a random blueprint depending on the selected parameters and spawn the unit */ + let unitBlueprint: UnitBlueprint | null; + if (forceCoalition) + unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { type: type, eras: activeEras, ranges: activeRanges, coalition: coalitionArea.getCoalition()}); + else + unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { type: type, eras: activeEras, ranges: activeRanges }); + + if (unitBlueprint) + this.spawnUnits("GroundUnit", [{ unitType: unitBlueprint.name, location: latlng, liveryID: "", skill: "High" }], coalitionArea.getCoalition(), false, "", ""); + } + } + } + } + }) + + citiesDatabase.forEach((city: { lat: number, lng: number, pop: number }) => { + /* Check if the city is inside the coalition area */ + if (polyContains(new LatLng(city.lat, city.lng), coalitionArea)) { + /* Arbitrary formula to obtain a number of units depending on the city population */ + var pointsNumber = 2 + Math.pow(city.pop, 0.15) * density / 100; + for (let i = 0; i < pointsNumber; i++) { + /* Place the unit nearby the city, depending on the distribution parameter */ + var bearing = Math.random() * 360; + var distance = Math.random() * distribution * 100; + const latlng = bearingAndDistanceToLatLng(city.lat, city.lng, bearing, distance); + + /* Make sure the unit is still inside the coalition area */ + if (polyContains(latlng, coalitionArea)) { + const type = activeTypes[Math.floor(Math.random() * activeTypes.length)]; + if (Math.random() < IADSDensities[type]) { + /* Get a random blueprint depending on the selected parameters and spawn the unit */ + let unitBlueprint: UnitBlueprint | null; + if (forceCoalition) + unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { type: type, eras: activeEras, ranges: activeRanges, coalition: coalitionArea.getCoalition()}); + else + unitBlueprint = randomUnitBlueprint(groundUnitDatabase, { type: type, eras: activeEras, ranges: activeRanges }); + + if (unitBlueprint) + this.spawnUnits("GroundUnit", [{ unitType: unitBlueprint.name, location: latlng, liveryID: "", skill: "High" }], coalitionArea.getCoalition(), false, "", ""); + } + } + } + } + }) + } + + /** Export all the ground and navy units to file. Does not work on Aircraft and Helicopter units. + * TODO: Extend to aircraft and helicopters + */ + exportToFile() { + if (!this.#unitDataExport) + this.#unitDataExport = new UnitDataFileExport("unit-export-dialog"); + this.#unitDataExport.showForm(Object.values(this.#units)); + } + + /** Import ground and navy units from file + * TODO: extend to support aircraft and helicopters + */ + importFromFile() { + if (!this.#unitDataImport) + this.#unitDataImport = new UnitDataFileImport("unit-import-dialog"); + this.#unitDataImport.selectFile(); + } + + /** Spawn a new group of units + * + * @param category Category of the new units + * @param units Array of unit tables + * @param coalition Coalition to which the new units will belong + * @param immediate If true the command will be performed immediately, but this may cause lag on the server + * @param airbase If true, the location of the units will be ignored and the units will spawn at the given airbase. Only works for aircrafts and helicopters + * @param country Set the country of the units. If empty string, the country will be assigned automatically + * @param callback CallableFunction called when the command is received by the server + * @returns True if the spawn command was successfully sent + */ + spawnUnits(category: string, units: UnitSpawnTable[], coalition: string = "blue", immediate: boolean = true, airbase: string = "", country: string = "", callback: CallableFunction = () => { }) { + var spawnPoints = 0; + var spawnFunction = () => { }; + var spawnsRestricted = getApp().getMissionManager().getCommandModeOptions().restrictSpawns && getApp().getMissionManager().getRemainingSetupTime() < 0 && getApp().getMissionManager().getCommandModeOptions().commandMode !== GAME_MASTER; + + if (category === "Aircraft") { + if (airbase == "" && spawnsRestricted) { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Aircrafts can be air spawned during the SETUP phase only"); + return false; + } + spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { return points + aircraftDatabase.getSpawnPointsByName(unit.unitType) }, 0); + spawnFunction = () => getApp().getServerManager().spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints, callback); + } else if (category === "Helicopter") { + if (airbase == "" && spawnsRestricted) { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Helicopters can be air spawned during the SETUP phase only"); + return false; + } + spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { return points + helicopterDatabase.getSpawnPointsByName(unit.unitType) }, 0); + spawnFunction = () => getApp().getServerManager().spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints, callback); + + } else if (category === "GroundUnit") { + if (spawnsRestricted) { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Ground units can be spawned during the SETUP phase only"); + return false; + } + spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { return points + groundUnitDatabase.getSpawnPointsByName(unit.unitType) }, 0); + spawnFunction = () => getApp().getServerManager().spawnGroundUnits(units, coalition, country, immediate, spawnPoints, callback); + + } else if (category === "NavyUnit") { + if (spawnsRestricted) { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Navy units can be spawned during the SETUP phase only"); + return false; + } + spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { return points + navyUnitDatabase.getSpawnPointsByName(unit.unitType) }, 0); + spawnFunction = () => getApp().getServerManager().spawnNavyUnits(units, coalition, country, immediate, spawnPoints, callback); + } + + if (spawnPoints <= getApp().getMissionManager().getAvailableSpawnPoints()) { + getApp().getMissionManager().setSpentSpawnPoints(spawnPoints); + spawnFunction(); + document.dispatchEvent( new CustomEvent( "unitSpawned", { + "detail": { + "airbase": airbase, + "category": category, + "coalition": coalition, + "country": country, + "immediate": immediate, + "unitSpawnTable": units + } + })); + return true; + } else { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Not enough spawn points available!"); + return false; + } + } + + /***********************************************/ + #onKeyUp(event: KeyboardEvent) { + if (!keyEventWasInInput(event)) { + if (event.key === "Delete") + this.delete(); + else if (event.key === "a" && event.ctrlKey) + Object.values(this.getUnits()).filter((unit: Unit) => { return !unit.getHidden() }).forEach((unit: Unit) => unit.setSelected(true)); + } + } + + #onUnitSelection(unit: Unit) { + if (this.getSelectedUnits().length > 0) { + /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ + if (!this.#selectionEventDisabled) { + getApp().getMap().setState(MOVE_UNIT); + window.setTimeout(() => { + document.dispatchEvent(new CustomEvent("unitsSelection", { detail: this.getSelectedUnits() })); + this.#selectionEventDisabled = false; + this.#showNumberOfSelectedProtectedUnits(); + }, 100); + this.#selectionEventDisabled = true; + } + } + else { + getApp().getMap().setState(IDLE); + document.dispatchEvent(new CustomEvent("clearSelection")); + } + } + + #onUnitDeselection(unit: Unit) { + if (this.getSelectedUnits().length == 0) { + getApp().getMap().setState(IDLE); + document.dispatchEvent(new CustomEvent("clearSelection")); + } + else { + /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ + if (!this.#deselectionEventDisabled) { + window.setTimeout(() => { + document.dispatchEvent(new CustomEvent("unitsDeselection", { detail: this.getSelectedUnits() })); + this.#deselectionEventDisabled = false; + }, 100); + this.#deselectionEventDisabled = true; + } + } + } + + #showActionMessage(units: Unit[], message: string) { + //if (units.length == 1) + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} ${message}`); + //else if (units.length > 1) + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`); + } + + #showSlowDeleteDialog(units: Unit[]) { + //let button: HTMLButtonElement | null = null; + //const deletionTime = Math.round(units.length * DELETE_CYCLE_TIME).toString(); + ////const dialog = this.#slowDeleteDialog; + //const element = dialog.getElement(); + //const listener = (ev: MouseEvent) => { + // if (ev.target instanceof HTMLButtonElement && ev.target.matches("[data-action]")) + // button = ev.target; + //} +// + //element.querySelectorAll(".deletion-count").forEach(el => el.innerHTML = units.length.toString()); + //element.querySelectorAll(".deletion-time").forEach(el => el.innerHTML = deletionTime); + //dialog.show(); +// + //return new Promise((resolve) => { + // element.addEventListener("click", listener); +// + // const interval = setInterval(() => { + // if (button instanceof HTMLButtonElement) { + // clearInterval(interval); + // dialog.hide(); + // element.removeEventListener("click", listener); + // resolve(button.getAttribute("data-action")); + // } + // }, 250); + //}); + } + + #showNumberOfSelectedProtectedUnits() { + const map = getApp().getMap(); + const units = this.getSelectedUnits(); + const numSelectedUnits = units.length; + const numProtectedUnits = units.filter((unit: Unit) => map.getIsUnitProtected(unit)).length; + + //if (numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits) + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: unit is protected`); + + //if (numProtectedUnits > 1) + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: selection contains ${numProtectedUnits} protected units.`); + } + + #unitIsProtected(unit: Unit) { + return getApp().getMap().getIsUnitProtected(unit) + } +} \ No newline at end of file diff --git a/frontend/react/src/weapon/weapon.ts b/frontend/react/src/weapon/weapon.ts new file mode 100644 index 00000000..996959bc --- /dev/null +++ b/frontend/react/src/weapon/weapon.ts @@ -0,0 +1,316 @@ +import { LatLng, DivIcon, Map } from 'leaflet'; +import { getApp } from '../olympusapp'; +import { enumToCoalition, mToFt, msToKnots, rad2deg, zeroAppend } from '../other/utils'; +import { CustomMarker } from '../map/markers/custommarker'; +import { SVGInjector } from '@tanem/svg-injector'; +import { DLINK, DataIndexes, GAME_MASTER, IRST, OPTIC, RADAR, VISUAL } from '../constants/constants'; +import { DataExtractor } from '../server/dataextractor'; +import { ObjectIconOptions } from '../interfaces'; + +export class Weapon extends CustomMarker { + ID: number; + + #alive: boolean = false; + #coalition: string = "neutral"; + #name: string = ""; + #position: LatLng = new LatLng(0, 0, 0); + #speed: number = 0; + #heading: number = 0; + + #hidden: boolean = false; + #detectionMethods: number[] = []; + + getAlive() {return this.#alive}; + getCoalition() {return this.#coalition}; + getName() {return this.#name}; + getPosition() {return this.#position}; + getSpeed() {return this.#speed}; + getHeading() {return this.#heading}; + + static getConstructor(type: string) { + if (type === "Missile") return Missile; + if (type === "Bomb") return Bomb; + } + + constructor(ID: number) { + super(new LatLng(0, 0), { riseOnHover: true, keyboard: false }); + + this.ID = ID; + + /* Update the marker when the options change */ + document.addEventListener("mapOptionsChanged", (ev: CustomEventInit) => { + this.#updateMarker(); + }); + } + + getCategory() { + // Overloaded by child classes + return ""; + } + + /********************** Unit data *************************/ + setData(dataExtractor: DataExtractor) { + var updateMarker = !getApp().getMap().hasLayer(this); + + var datumIndex = 0; + while (datumIndex != DataIndexes.endOfData) { + datumIndex = dataExtractor.extractUInt8(); + switch (datumIndex) { + case DataIndexes.category: dataExtractor.extractString(); break; + case DataIndexes.alive: this.setAlive(dataExtractor.extractBool()); updateMarker = true; break; + case DataIndexes.coalition: this.#coalition = enumToCoalition(dataExtractor.extractUInt8()); break; + case DataIndexes.name: this.#name = dataExtractor.extractString(); break; + case DataIndexes.position: this.#position = dataExtractor.extractLatLng(); updateMarker = true; break; + case DataIndexes.speed: this.#speed = dataExtractor.extractFloat64(); updateMarker = true; break; + case DataIndexes.heading: this.#heading = dataExtractor.extractFloat64(); updateMarker = true; break; + } + } + + if (updateMarker) + this.#updateMarker(); + } + + getData() { + return { + category: this.getCategory(), + ID: this.ID, + alive: this.#alive, + coalition: this.#coalition, + name: this.#name, + position: this.#position, + speed: this.#speed, + heading: this.#heading + } + } + + getMarkerCategory(): string { + return ""; + } + + getIconOptions(): ObjectIconOptions { + // Default values, overloaded by child classes if needed + return { + showState: false, + showVvi: false, + showHealth: false, + showHotgroup: false, + showUnitIcon: true, + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: true, + showCallsign: true, + rotateToHeading: false + } + } + + setAlive(newAlive: boolean) { + this.#alive = newAlive; + } + + belongsToCommandedCoalition() { + if (getApp().getMissionManager().getCommandModeOptions().commandMode !== GAME_MASTER && getApp().getMissionManager().getCommandedCoalition() !== this.#coalition) + return false; + return true; + } + + getType() { + return ""; + } + + /********************** Icon *************************/ + createIcon(): void { + /* Set the icon */ + var icon = new DivIcon({ + className: 'leaflet-unit-icon', + iconAnchor: [25, 25], + iconSize: [50, 50], + }); + this.setIcon(icon); + + var el = document.createElement("div"); + el.classList.add("unit"); + el.setAttribute("data-object", `unit-${this.getMarkerCategory()}`); + el.setAttribute("data-coalition", this.#coalition); + + // Generate and append elements depending on active options + // Velocity vector + if (this.getIconOptions().showVvi) { + var vvi = document.createElement("div"); + vvi.classList.add("unit-vvi"); + vvi.toggleAttribute("data-rotate-to-heading"); + el.append(vvi); + } + + // Main icon + if (this.getIconOptions().showUnitIcon) { + var unitIcon = document.createElement("div"); + unitIcon.classList.add("unit-icon"); + var img = document.createElement("img"); + img.src = `/resources/theme/images/units/${this.getMarkerCategory()}.svg`; + img.onload = () => SVGInjector(img); + unitIcon.appendChild(img); + unitIcon.toggleAttribute("data-rotate-to-heading", this.getIconOptions().rotateToHeading); + el.append(unitIcon); + } + + this.getElement()?.appendChild(el); + } + + /********************** Visibility *************************/ + updateVisibility() { + const hiddenUnits = getApp().getMap().getHiddenTypes(); + var hidden = (hiddenUnits.includes(this.getMarkerCategory())) || + (hiddenUnits.includes(this.#coalition)) || + (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0); + + this.setHidden(hidden || !this.#alive); + } + + setHidden(hidden: boolean) { + this.#hidden = hidden; + + /* Add the marker if not present */ + if (!getApp().getMap().hasLayer(this) && !this.getHidden()) { + if (getApp().getMap().isZooming()) + this.once("zoomend", () => {this.addTo(getApp().getMap())}) + else + this.addTo(getApp().getMap()); + } + + /* Hide the marker if necessary*/ + if (getApp().getMap().hasLayer(this) && this.getHidden()) { + getApp().getMap().removeLayer(this); + } + } + + getHidden() { + return this.#hidden; + } + + setDetectionMethods(newDetectionMethods: number[]) { + if (!this.belongsToCommandedCoalition()) { + /* Check if the detection methods of this unit have changed */ + if (this.#detectionMethods.length !== newDetectionMethods.length || this.getDetectionMethods().some(value => !newDetectionMethods.includes(value))) { + /* Force a redraw of the unit to reflect the new status of the detection methods */ + this.setHidden(true); + this.#detectionMethods = newDetectionMethods; + this.#updateMarker(); + } + } + } + + getDetectionMethods() { + return this.#detectionMethods; + } + + /***********************************************/ + onAdd(map: Map): this { + super.onAdd(map); + return this; + } + + #updateMarker() { + this.updateVisibility(); + + /* Draw the marker */ + if (!this.getHidden()) { + if (this.getLatLng().lat !== this.#position.lat || this.getLatLng().lng !== this.#position.lng) { + this.setLatLng(new LatLng(this.#position.lat, this.#position.lng)); + } + + var element = this.getElement(); + if (element != null) { + /* Draw the velocity vector */ + element.querySelector(".unit-vvi")?.setAttribute("style", `height: ${15 + this.#speed / 5}px;`); + + /* Set dead/alive flag */ + element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.#alive); + + + /* Set altitude and speed */ + if (element.querySelector(".unit-altitude")) + (element.querySelector(".unit-altitude")).innerText = "FL" + zeroAppend(Math.floor(mToFt(this.#position.alt as number) / 100), 3); + if (element.querySelector(".unit-speed")) + (element.querySelector(".unit-speed")).innerText = String(Math.floor(msToKnots(this.#speed))) + "GS"; + + /* Rotate elements according to heading */ + element.querySelectorAll("[data-rotate-to-heading]").forEach(el => { + const headingDeg = rad2deg(this.#heading); + let currentStyle = el.getAttribute("style") || ""; + el.setAttribute("style", currentStyle + `transform:rotate(${headingDeg}deg);`); + }); + } + + /* Set vertical offset for altitude stacking */ + var pos = getApp().getMap().latLngToLayerPoint(this.getLatLng()).round(); + this.setZIndexOffset(1000 + Math.floor(this.#position.alt as number) - pos.y); + } + } +} + +export class Missile extends Weapon { + constructor(ID: number) { + super(ID); + } + + getCategory() { + return "Missile"; + } + + getMarkerCategory() { + if (this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC)) + return "missile"; + else + return "aircraft"; + } + + getIconOptions() { + return { + showState: false, + showVvi: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))), + showHealth: false, + showHotgroup: false, + showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))), + showCallsign: false, + rotateToHeading: this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC) + }; + } +} + +export class Bomb extends Weapon { + constructor(ID: number) { + super(ID); + } + + getCategory() { + return "Bomb"; + } + + getMarkerCategory() { + if (this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC)) + return "bomb"; + else + return "aircraft"; + } + + getIconOptions() { + return { + showState: false, + showVvi: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))), + showHealth: false, + showHotgroup: false, + showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))), + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))), + showCallsign: false, + rotateToHeading: this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC) + }; + } +} diff --git a/frontend/react/src/weapon/weaponsmanager.ts b/frontend/react/src/weapon/weaponsmanager.ts new file mode 100644 index 00000000..280568c3 --- /dev/null +++ b/frontend/react/src/weapon/weaponsmanager.ts @@ -0,0 +1,109 @@ +import { getApp } from "../olympusapp"; +import { Weapon } from "./weapon"; +import { DataIndexes } from "../constants/constants"; +import { DataExtractor } from "../server/dataextractor"; +import { Contact } from "../interfaces"; + + +/** The WeaponsManager handles the creation and update of weapons. Data is strictly updated by the server ONLY. */ +export class WeaponsManager { + #weapons: { [ID: number]: Weapon }; + + constructor() { + this.#weapons = {}; + + document.addEventListener("commandModeOptionsChanged", () => {Object.values(this.#weapons).forEach((weapon: Weapon) => weapon.updateVisibility())}); + } + + /** + * + * @returns All the existing weapons, both active and destroyed + */ + getWeapons() { + return this.#weapons; + } + + /** Get a weapon by ID + * + * @param ID ID of the weapon + * @returns Weapon object, or null if input ID does not exist + */ + getWeaponByID(ID: number) { + if (ID in this.#weapons) + return this.#weapons[ID]; + else + return null; + } + + /** Add a new weapon to the manager + * + * @param ID ID of the new weapon + * @param category Either "Missile" or "Bomb". Determines what class will be used to create the new unit accordingly. + */ + addWeapon(ID: number, category: string) { + if (category){ + /* The name of the weapon category is exactly the same as the constructor name */ + var constructor = Weapon.getConstructor(category); + if (constructor != undefined) { + this.#weapons[ID] = new constructor(ID); + } + } + } + + /** Update the data of all the weapons. The data is directly decoded from the binary buffer received from the REST Server. This is necessary for performance and bandwidth reasons. + * + * @param buffer The arraybuffer, encoded according to the ICD defined in: TODO Add reference to ICD + * @returns The decoded updateTime of the data update. + */ + update(buffer: ArrayBuffer) { + /* Extract the data from the arraybuffer. Since data is encoded dynamically (not all data is always present, but rather only the data that was actually updated since the last request). + No a prori casting can be performed. On the contrary, the array is decoded incrementally, depending on the DataIndexes of the data. The actual data decoding is performed by the Weapon class directly. + Every time a piece of data is decoded the decoder seeker is incremented. */ + var dataExtractor = new DataExtractor(buffer); + + var updateTime = Number(dataExtractor.extractUInt64()); + + /* Run until all data is extracted or an error occurs */ + while (dataExtractor.getSeekPosition() < buffer.byteLength) { + /* Extract the weapon ID */ + const ID = dataExtractor.extractUInt32(); + + /* If the ID of the weapon does not yet exist, create the weapon, if the category is known. If it isn't, some data must have been lost and we need to wait for another update */ + if (!(ID in this.#weapons)) { + const datumIndex = dataExtractor.extractUInt8(); + if (datumIndex == DataIndexes.category) { + const category = dataExtractor.extractString(); + this.addWeapon(ID, category); + } + else { + /* Inconsistent data, we need to wait for a refresh */ + return updateTime; + } + } + /* Update the data of the weapon */ + this.#weapons[ID]?.setData(dataExtractor); + } + return updateTime; + } + + /** For a given weapon, it returns if and how it is being detected by other units. NOTE: this function will return how a weapon is being detected, i.e. how other units are detecting it. It will not return + * what the weapon is detecting (mostly because weapons can't detect units). + * + * @param weapon The unit of which to retrieve the "detected" methods. + * @returns Array of detection methods + */ + getWeaponDetectedMethods(weapon: Weapon) { + var detectionMethods: number[] = []; + var units = getApp().getUnitsManager().getUnits(); + for (let idx in units) { + if (units[idx].getAlive() && units[idx].getIsLeader() && units[idx].getCoalition() !== "neutral" && units[idx].getCoalition() != weapon.getCoalition()) + { + units[idx].getContacts().forEach((contact: Contact) => { + if (contact.ID == weapon.ID && !detectionMethods.includes(contact.detectionMethod)) + detectionMethods.push(contact.detectionMethod); + }); + } + } + return detectionMethods; + } +} \ No newline at end of file diff --git a/frontend/react/tailwind.config.js b/frontend/react/tailwind.config.js index 2042fe07..1bfa1b3f 100644 --- a/frontend/react/tailwind.config.js +++ b/frontend/react/tailwind.config.js @@ -8,7 +8,9 @@ export default { extend: { colors: { background: { - steel: "#202831" + darker: "#202831", + dark: "#2D3742", + neutral: "#394552" } } }, diff --git a/frontend/server/app.js b/frontend/server/app.js index b07fe860..a16b6eb1 100644 --- a/frontend/server/app.js +++ b/frontend/server/app.js @@ -5,6 +5,7 @@ module.exports = function (configLocation) { var logger = require('morgan'); var fs = require('fs'); var bodyParser = require('body-parser'); + var cors = require('cors'); const { createProxyMiddleware } = require('http-proxy-middleware'); /* Default routers */ @@ -39,6 +40,7 @@ module.exports = function (configLocation) { app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(express.static(path.join(__dirname, 'public'))); + app.use(cors()) /* Apply routers */ app.use('/', indexRouter); diff --git a/frontend/server/demo.js b/frontend/server/demo.js index 23d18096..90c3694f 100644 --- a/frontend/server/demo.js +++ b/frontend/server/demo.js @@ -3,6 +3,7 @@ module.exports = function (configLocation) { var logger = require('morgan'); var enc = new TextEncoder(); const path = require('path'); + var cors = require('cors'); var express = require('express'); var fs = require('fs'); @@ -13,6 +14,7 @@ module.exports = function (configLocation) { var app = express(); app.use(logger('dev')); + app.use(cors()) const aircraftDatabase = require(path.join(path.dirname(configLocation), '../Mods/Services/Olympus/databases/units/aircraftdatabase.json')); const helicopterDatabase = require(path.join(path.dirname(configLocation),'../Mods/Services/Olympus/databases/units/helicopterdatabase.json')); @@ -458,7 +460,7 @@ module.exports = function (configLocation) { }; mission(req, res){ - var ret = {mission: {theatre: "MarianaIslands"}}; + var ret = {mission: {theatre: "Nevada"}}; ret.time = Date.now(); ret.mission.dateAndTime = { diff --git a/frontend/server/package.json b/frontend/server/package.json index 7acdf8e1..54946d05 100644 --- a/frontend/server/package.json +++ b/frontend/server/package.json @@ -27,5 +27,8 @@ "tcp-ping-port": "^1.0.1", "uuid": "^9.0.1", "yargs": "^17.7.2" + }, + "devDependencies": { + "cors": "^2.8.5" } }