From 61d6d9f16c04bd1407c158f13bb83e0fc6e22f67 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 20 Jun 2023 17:36:21 +0200 Subject: [PATCH] Added better temporary markers and more map functions --- client/public/stylesheets/markers/units.css | 4 + .../public/stylesheets/other/contextmenus.css | 7 +- .../olympus/images/buttons/other/back.svg | 41 +++ .../images/buttons/tools/pen-solid.svg | 1 + client/src/constants/constants.ts | 13 +- client/src/controls/mapcontextmenu.ts | 14 +- client/src/map/boxselect.ts | 2 +- client/src/map/coalitionarea.ts | 25 +- client/src/map/map.ts | 233 ++++++++++-------- client/src/map/temporaryunitmarker.ts | 49 +++- client/src/other/utils.ts | 28 +++ client/src/server/server.ts | 4 +- client/src/units/unit.ts | 49 ++-- client/src/units/unitsmanager.ts | 15 +- client/views/other/contextmenus.ejs | 2 + client/views/panels/navbar.ejs | 3 + 16 files changed, 324 insertions(+), 166 deletions(-) create mode 100644 client/public/themes/olympus/images/buttons/other/back.svg create mode 100644 client/public/themes/olympus/images/buttons/tools/pen-solid.svg diff --git a/client/public/stylesheets/markers/units.css b/client/public/stylesheets/markers/units.css index 65ec1d4b..62ca82b6 100644 --- a/client/public/stylesheets/markers/units.css +++ b/client/public/stylesheets/markers/units.css @@ -311,4 +311,8 @@ [data-object|="unit-aircraft"][data-is-dead] .unit-summary .unit-callsign { display: block; +} + +.ol-temporary-marker { + opacity: 0.5; } \ No newline at end of file diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css index 9e5754a4..99e8b35c 100644 --- a/client/public/stylesheets/other/contextmenus.css +++ b/client/public/stylesheets/other/contextmenus.css @@ -350,7 +350,7 @@ height: fit-content; position: absolute; row-gap: 5px; - width: 225px; + width: 250px; z-index: 9999; } @@ -370,6 +370,11 @@ background-size: 48px; } +#coalitionarea-back-button { + background-image: url("/resources/theme/images/buttons/other/back.svg"); + background-size: 48px; +} + #coalitionarea-delete-button { background-image: url("/resources/theme/images/buttons/other/delete.svg"); background-size: 48px; diff --git a/client/public/themes/olympus/images/buttons/other/back.svg b/client/public/themes/olympus/images/buttons/other/back.svg new file mode 100644 index 00000000..52c98f94 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/other/back.svg @@ -0,0 +1,41 @@ + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/tools/pen-solid.svg b/client/public/themes/olympus/images/buttons/tools/pen-solid.svg new file mode 100644 index 00000000..a690992f --- /dev/null +++ b/client/public/themes/olympus/images/buttons/tools/pen-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 0f91ed25..a230e17d 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -99,4 +99,15 @@ export const layers = { maxZoom: 20, attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' } -} \ No newline at end of file +} + +/* Map constants */ +export const IDLE = "Idle"; +export const MOVE_UNIT = "Move unit"; +export const BOMBING = "Bombing"; +export const CARPET_BOMBING = "Carpet bombing"; +export const FIRE_AT_AREA = "Fire at area"; +export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area"; +export const COALITIONAREA_INTERACT = "Interact with Coalition Areas" +export const visibilityControls: string[] = ["human", "dcs", "aircraft", "groundunit-sam", "groundunit-other", "navyunit", "airbase"]; +export const visibilityControlsTootlips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; diff --git a/client/src/controls/mapcontextmenu.ts b/client/src/controls/mapcontextmenu.ts index d90e96f2..c0cdcdf9 100644 --- a/client/src/controls/mapcontextmenu.ts +++ b/client/src/controls/mapcontextmenu.ts @@ -11,7 +11,7 @@ import { ftToM } from "../other/utils"; export interface SpawnOptions { role: string; - type: string; + name: string; latlng: LatLng; coalition: string; loadout?: string | null; @@ -28,7 +28,7 @@ export class MapContextMenu extends ContextMenu { #aircrafSpawnAltitudeSlider: Slider; #groundUnitRoleDropdown: Dropdown; #groundUnitTypeDropdown: Dropdown; - #spawnOptions: SpawnOptions = { role: "", type: "", latlng: new LatLng(0, 0), loadout: null, coalition: "blue", airbaseName: null, altitude: ftToM(20000) }; + #spawnOptions: SpawnOptions = { role: "", name: "", latlng: new LatLng(0, 0), loadout: null, coalition: "blue", airbaseName: null, altitude: ftToM(20000) }; constructor(id: string) { super(id); @@ -57,7 +57,7 @@ export class MapContextMenu extends ContextMenu { this.hide(); this.#spawnOptions.coalition = getActiveCoalition(); if (this.#spawnOptions) { - getMap().addTemporaryMarker(this.#spawnOptions.latlng); + getMap().addTemporaryMarker(this.#spawnOptions); spawnAircraft(this.#spawnOptions); } }); @@ -66,7 +66,7 @@ export class MapContextMenu extends ContextMenu { this.hide(); this.#spawnOptions.coalition = getActiveCoalition(); if (this.#spawnOptions) { - getMap().addTemporaryMarker(this.#spawnOptions.latlng); + getMap().addTemporaryMarker(this.#spawnOptions); spawnGroundUnit(this.#spawnOptions); } }); @@ -179,7 +179,7 @@ export class MapContextMenu extends ContextMenu { this.#resetAircraftType(); var type = aircraftDatabase.getByLabel(label)?.name || null; if (type != null) { - this.#spawnOptions.type = type; + this.#spawnOptions.name = type; this.#aircraftLoadoutDropdown.setOptions(aircraftDatabase.getLoadoutNamesByRole(type, this.#spawnOptions.role)); this.#aircraftLoadoutDropdown.selectValue(0); var image = (this.getContainer()?.querySelector("#unit-image")); @@ -198,7 +198,7 @@ export class MapContextMenu extends ContextMenu { } #setAircraftLoadout(loadoutName: string) { - var loadout = aircraftDatabase.getLoadoutByName(this.#spawnOptions.type, loadoutName); + var loadout = aircraftDatabase.getLoadoutByName(this.#spawnOptions.name, loadoutName); if (loadout) { this.#spawnOptions.loadout = loadout.code; (this.getContainer()?.querySelector("#aircraft-spawn-menu")?.querySelector(".deploy-unit-button")).disabled = false; @@ -241,7 +241,7 @@ export class MapContextMenu extends ContextMenu { this.#resetGroundUnitType(); var type = groundUnitsDatabase.getByLabel(label)?.name || null; if (type != null) { - this.#spawnOptions.type = type; + this.#spawnOptions.name = type; (this.getContainer()?.querySelector("#ground-unit-spawn-menu")?.querySelector(".deploy-unit-button")).disabled = false; } this.clip(); diff --git a/client/src/map/boxselect.ts b/client/src/map/boxselect.ts index 2ea92fd8..a37f4ace 100644 --- a/client/src/map/boxselect.ts +++ b/client/src/map/boxselect.ts @@ -46,7 +46,7 @@ export var BoxSelect = Handler.extend({ _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(); diff --git a/client/src/map/coalitionarea.ts b/client/src/map/coalitionarea.ts index b5e61be0..2f44d9f5 100644 --- a/client/src/map/coalitionarea.ts +++ b/client/src/map/coalitionarea.ts @@ -1,4 +1,4 @@ -import { LatLng, LatLngExpression, Map, Point, Polygon, PolylineOptions } from "leaflet"; +import { DomUtil, LatLng, LatLngExpression, Map, Point, Polygon, PolylineOptions } from "leaflet"; import { getMap } from ".."; import { CoalitionAreaHandle } from "./coalitionareahandle"; import { CoalitionAreaMiddleHandle } from "./coalitionareamiddlehandle"; @@ -19,6 +19,7 @@ export class CoalitionArea extends Polygon { super(latlngs, options); this.#setColors(); this.#registerCallbacks(); + } setCoalition(coalition: string) { @@ -61,6 +62,17 @@ export class CoalitionArea extends Polygon { return this.#editing; } + setInteractive(interactive: boolean) { + this.setOpacity(interactive? 1: 0.5); + this.options.interactive = interactive; + + if (interactive) { + DomUtil.addClass(this.getElement() as HTMLElement, 'leaflet-interactive'); + } else { + DomUtil.removeClass(this.getElement() as HTMLElement, 'leaflet-interactive'); + } + } + addTemporaryLatLng(latlng: LatLng) { this.#activeIndex++; var latlngs = this.getLatLngs()[0] as LatLng[]; @@ -69,13 +81,17 @@ export class CoalitionArea extends Polygon { this.#setHandles(); } - moveTemporaryLatLng(latlng: LatLng) { + 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}); + } + #setColors() { const coalitionColor = this.getCoalition() === "blue" ? "#247be2" : "#ff5858"; this.setStyle({ color: this.getSelected() ? "white" : coalitionColor, fillColor: coalitionColor }); @@ -138,8 +154,11 @@ export class CoalitionArea extends Polygon { }); this.on("contextmenu", (e: any) => { - if (this.getSelected() && !this.getEditing()) + if (!this.getEditing()) { + getMap().deselectAllCoalitionAreas(); + this.setSelected(true); getMap().showCoalitionAreaContextMenu(e, this); + } else this.setEditing(false); }); diff --git a/client/src/map/map.ts b/client/src/map/map.ts index e165f2db..864f4783 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -12,7 +12,7 @@ import { DestinationPreviewMarker } from "./destinationpreviewmarker"; import { TemporaryUnitMarker } from "./temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { SVGInjector } from '@tanem/svg-injector' -import { layers as mapLayers, mapBounds, minimapBoundaries } from "../constants/constants"; +import { layers as mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, visibilityControls, visibilityControlsTootlips, FIRE_AT_AREA, MOVE_UNIT, CARPET_BOMBING, BOMBING, COALITIONAREA_INTERACT } from "../constants/constants"; import { TargetMarker } from "./targetmarker"; import { CoalitionArea } from "./coalitionarea"; import { CoalitionAreaContextMenu } from "../controls/coalitionareacontextmenu"; @@ -24,16 +24,6 @@ L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); require("../../public/javascripts/leaflet.nauticscale.js") require("../../public/javascripts/L.Path.Drag.js") -/* Map constants */ -export const IDLE = "Idle"; -export const MOVE_UNIT = "Move unit"; -export const BOMBING = "Bombing"; -export const CARPET_BOMBING = "Carpet bombing"; -export const FIRE_AT_AREA = "Fire at area"; -export const DRAW_COALITIONAREA_POLYGON = "Draw Coalition Area"; -export const visibilityControls: string[] = ["human", "dcs", "aircraft", "groundunit-sam", "groundunit-other", "navyunit", "airbase"]; -export const visibilityControlsTootlips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; - export class Map extends L.Map { #ID: string; #state: string; @@ -51,16 +41,17 @@ export class Map extends L.Map { #miniMap: ClickableMiniMap | null = null; #miniMapLayerGroup: L.LayerGroup; #temporaryMarkers: TemporaryUnitMarker[] = []; - + #selecting: boolean = false; + #destinationGroupRotation: number = 0; #computeDestinationRotation: boolean = false; #destinationRotationCenter: L.LatLng | null = null; #coalitionAreas: CoalitionArea[] = []; - #targetCursor: TargetMarker = new TargetMarker(new L.LatLng(0, 0), {interactive: false}); + #targetCursor: TargetMarker = new TargetMarker(new L.LatLng(0, 0), { interactive: false }); #destinationPreviewCursors: DestinationPreviewMarker[] = []; #drawingCursor: DrawingCursor = new DrawingCursor(); - + #mapContextMenu: MapContextMenu = new MapContextMenu("map-contextmenu"); #unitContextMenu: UnitContextMenu = new UnitContextMenu("unit-contextmenu"); #airbaseContextMenu: AirbaseContextMenu = new AirbaseContextMenu("airbase-contextmenu"); @@ -71,8 +62,7 @@ export class Map extends L.Map { constructor(ID: string) { /* Init the leaflet map */ - - //@ts-ignore + //@ts-ignore Needed because the boxSelect option is non-standard super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); this.setView([37.23, -115.8], 10); @@ -102,6 +92,7 @@ export class Map extends L.Map { this.on("zoomstart", (e: any) => this.#onZoom(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)); @@ -113,7 +104,7 @@ export class Map extends L.Map { document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { const el = ev.detail._element; el?.classList.toggle("off"); - getUnitsManager().setHiddenType(ev.detail.coalition, (el?.currentTarget as HTMLElement)?.classList.contains("off")); + getUnitsManager().setHiddenType(ev.detail.coalition, !el?.classList.contains("off")); Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); }); @@ -124,16 +115,28 @@ export class Map extends L.Map { Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); }); + document.addEventListener("toggleCoalitionAreaInteraction", (ev: CustomEventInit) => { + const el = ev.detail._element; + /* Add listener to set the button to off if the state changes */ + document.addEventListener("mapStateChanged", () => el?.classList.toggle("off", !(this.getState() === COALITIONAREA_INTERACT))); + if (this.getState() !== COALITIONAREA_INTERACT) + this.setState(COALITIONAREA_INTERACT); + else + this.setState(IDLE); + }); + document.addEventListener("toggleCoalitionAreaDraw", (ev: CustomEventInit) => { const el = ev.detail._element; - document.addEventListener("mapStateChanged", () => el?.classList.toggle("off", !(this.getState() === DRAW_COALITIONAREA_POLYGON))); + /* Add listener to set the button to off if the state changes */ + document.addEventListener("mapStateChanged", () => el?.classList.toggle("off", !(this.getState() === COALITIONAREA_DRAW_POLYGON))); + this.deselectAllCoalitionAreas(); if (ev.detail?.type == "polygon") { - if (this.getState() !== DRAW_COALITIONAREA_POLYGON) - this.setState(DRAW_COALITIONAREA_POLYGON); - else + 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) @@ -143,7 +146,7 @@ export class Map extends L.Map { /* Pan interval */ this.#panInterval = window.setInterval(() => { if (this.#panLeft || this.#panDown || this.#panRight || this.#panLeft) - this.panBy(new L.Point(((this.#panLeft? -1 : 0) + (this.#panRight ? 1 : 0)) * this.#deafultPanDelta, + this.panBy(new L.Point(((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.#deafultPanDelta, ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.#deafultPanDelta)); }, 20); @@ -155,10 +158,10 @@ export class Map extends L.Map { } setLayer(layerName: string) { - if (this.#layer != null) + if (this.#layer != null) this.removeLayer(this.#layer) - - if (layerName in mapLayers){ + + if (layerName in mapLayers) { const layerData = mapLayers[layerName as keyof typeof mapLayers]; var options: L.TileLayerOptions = { attribution: layerData.attribution, @@ -178,17 +181,25 @@ export class Map extends L.Map { /* State machine */ setState(state: string) { this.#state = state; - this.#showCursor(); - if (this.#state === IDLE) { + this.#updateCursor(); + + /* Operations to perform if you are NOT in a state */ + if (this.#state !== COALITIONAREA_INTERACT) { + this.#coalitionAreas.forEach((coalitionArea: CoalitionArea) => { + coalitionArea.setInteractive(false); + }); + } + if (this.#state !== COALITIONAREA_DRAW_POLYGON) { this.#deselectCoalitionAreas(); } - else if (this.#state === MOVE_UNIT) { - this.#deselectCoalitionAreas(); + + /* Operations to perform if you ARE in a state */ + if (this.#state === COALITIONAREA_INTERACT) { + this.#coalitionAreas.forEach((coalitionArea: CoalitionArea) => { + coalitionArea.setInteractive(true); + }); } - else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) { - this.#deselectCoalitionAreas(); - } - else if (this.#state === DRAW_COALITIONAREA_POLYGON) { + else if (this.#state === COALITIONAREA_DRAW_POLYGON) { this.#coalitionAreas.push(new CoalitionArea([])); this.#coalitionAreas[this.#coalitionAreas.length - 1].addTo(this); } @@ -377,13 +388,14 @@ export class Map extends L.Map { } } - addTemporaryMarker(latlng: L.LatLng) { - var marker = new TemporaryUnitMarker(latlng); + addTemporaryMarker(spawnOptions: SpawnOptions) { + var marker = new TemporaryUnitMarker(spawnOptions); marker.addTo(this); this.#temporaryMarkers.push(marker); } removeTemporaryMarker(latlng: L.LatLng) { + // TODO something more refined than this var d: number | null = null; var closest: L.Marker | null = null; var i: number = 0; @@ -402,7 +414,7 @@ export class Map extends L.Map { } getSelectedCoalitionArea() { - return this.#coalitionAreas.find((area: CoalitionArea) => {return area.getSelected()}); + return this.#coalitionAreas.find((area: CoalitionArea) => { return area.getSelected() }); } /* Event handlers */ @@ -412,7 +424,7 @@ export class Map extends L.Map { if (this.#state === IDLE) { this.deselectAllCoalitionAreas(); } - else if (this.#state === DRAW_COALITIONAREA_POLYGON) { + else if (this.#state === COALITIONAREA_DRAW_POLYGON) { if (this.getSelectedCoalitionArea()?.getEditing()) { this.getSelectedCoalitionArea()?.addTemporaryLatLng(e.latlng); } @@ -443,41 +455,48 @@ export class Map extends L.Map { getUnitsManager().selectedUnitsClearDestinations(); } getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, e.originalEvent.shiftKey, this.#destinationGroupRotation) - this.#destinationGroupRotation = 0; + this.#destinationGroupRotation = 0; this.#destinationRotationCenter = null; this.#computeDestinationRotation = false; } else if (this.#state === BOMBING) { - getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); - getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); + getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE); + getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); } else if (this.#state === CARPET_BOMBING) { - getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); - getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); + getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE); + getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); } else if (this.#state === FIRE_AT_AREA) { - getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); - getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); + getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE); + getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); } 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); getUnitsManager().selectFromBounds(e.selectionBounds); + this.#updateCursor(); } #onMouseDown(e: any) { this.hideAllContextMenus(); if (this.#state == MOVE_UNIT) { - this.#destinationGroupRotation = 0; + this.#destinationGroupRotation = 0; this.#destinationRotationCenter = null; this.#computeDestinationRotation = false; if (e.originalEvent.button == 2) { @@ -494,32 +513,32 @@ export class Map extends L.Map { this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; - this.#showCursor(e); + this.#updateCursor(e); - if (this.#state === MOVE_UNIT){ + 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.#updateDestinationPreview(e); + this.#updateDestinationCursors(e); } else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) { this.#targetCursor.setLatLng(this.getMouseCoordinates()); } - else if (this.#state === DRAW_COALITIONAREA_POLYGON) { - if (this.getSelectedCoalitionArea()?.getEditing() && !e.originalEvent.ctrlKey){ - this.#drawingCursor.setLatLng(e.latlng); - this.getSelectedCoalitionArea()?.moveTemporaryLatLng(e.latlng); - } + else if (this.#state === COALITIONAREA_DRAW_POLYGON) { + 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.#updateDestinationPreview(e); - this.#showCursor(e); + this.#updateDestinationCursors(e); + this.#updateCursor(e); } #onKeyUp(e: any) { - this.#updateDestinationPreview(e); - this.#showCursor(e); + this.#updateDestinationCursors(e); + this.#updateCursor(e); } #onZoom(e: any) { @@ -537,13 +556,6 @@ export class Map extends L.Map { return minimapBoundaries; } - #updateDestinationPreview(e: any) { - Object.values(getUnitsManager().selectedUnitsComputeGroupDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : this.getMouseCoordinates(), this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => { - if (idx < this.#destinationPreviewCursors.length) - this.#destinationPreviewCursors[idx].setLatLng(e.originalEvent.shiftKey ? latlng : this.getMouseCoordinates()); - }) - } - #createOptionButton(value: string, url: string, title: string, callback: string, argument: string) { var button = document.createElement("button"); const img = document.createElement("img"); @@ -557,32 +569,58 @@ export class Map extends L.Map { return button; } - #showDestinationCursors() { - this.#hideDestinationCursors(); + #deselectCoalitionAreas() { + this.getSelectedCoalitionArea()?.setSelected(false); + } - if (getUnitsManager().getSelectedUnits({ excludeHumans: true }).length > 0) { - /* Create the unit destination preview markers */ - this.#destinationPreviewCursors = getUnitsManager().getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }).map((unit: Unit) => { - var marker = new DestinationPreviewMarker(this.getMouseCoordinates(), {interactive: false}); - marker.addTo(this); - return marker; - }) + /* Cursors */ + #showDefaultCursor() { + document.getElementById(this.#ID)?.classList.remove("hidden-cursor"); + } + + #hideDefaultCursor() { + document.getElementById(this.#ID)?.classList.add("hidden-cursor"); + } + + #showDestinationCursors() { + /* Don't create the cursors if there already are the correct number of them available */ + if (getUnitsManager().getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }).length != this.#destinationPreviewCursors.length) { + /* Reset the cursors to start from a clean condition */ + this.#hideDestinationCursors(); + + if (getUnitsManager().getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }).length > 0) { + /* Create the cursors. If a group is selected only one cursor is shown for it, because you can't control single units in a group */ + this.#destinationPreviewCursors = getUnitsManager().getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }).map((unit: Unit) => { + var marker = new DestinationPreviewMarker(this.getMouseCoordinates(), { interactive: false }); + marker.addTo(this); + return marker; + }) + } } } + #updateDestinationCursors(e: any) { + const groupLatLng = this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : this.getMouseCoordinates(); + Object.values(getUnitsManager().selectedUnitsComputeGroupDestination(groupLatLng, this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => { + if (idx < this.#destinationPreviewCursors.length) + this.#destinationPreviewCursors[idx].setLatLng(e.originalEvent.shiftKey ? latlng : this.getMouseCoordinates()); + }) + } + #hideDestinationCursors() { - /* Remove all the destination preview markers */ + /* Remove all the destination cursors */ this.#destinationPreviewCursors.forEach((marker: L.Marker) => { this.removeLayer(marker); }) this.#destinationPreviewCursors = []; + /* Reset the variables used to compute the rotation of the group cursors */ this.#destinationGroupRotation = 0; this.#computeDestinationRotation = false; this.#destinationRotationCenter = null; } - #showTargetCursor(){ + #showTargetCursor() { this.#hideTargetCursor(); this.#targetCursor.addTo(this); } @@ -592,18 +630,6 @@ export class Map extends L.Map { this.removeLayer(this.#targetCursor); } - #deselectCoalitionAreas() { - this.getSelectedCoalitionArea()?.setSelected(false); - } - - #showDefaultCursor() { - document.getElementById(this.#ID)?.classList.remove("hidden-cursor"); - } - - #hideDefaultCursor() { - document.getElementById(this.#ID)?.classList.add("hidden-cursor"); - } - #showDrawingCursor() { this.#hideDefaultCursor(); if (!this.hasLayer(this.#drawingCursor)) @@ -611,38 +637,33 @@ export class Map extends L.Map { } #hideDrawingCursor() { + this.#drawingCursor.setLatLng(new L.LatLng(0, 0)); if (this.hasLayer(this.#drawingCursor)) this.#drawingCursor.removeFrom(this); } - #showCursor(e?: any) { - if (e?.originalEvent.ctrlKey) { - this.#hideDefaultCursor(); + #updateCursor(e?: any) { + /* If the ctrl key is being pressed or we are performing an area selection, show the default cursor */ + if (e?.originalEvent.ctrlKey || this.#selecting) { + /* Hide all non default cursors */ this.#hideDestinationCursors(); this.#hideTargetCursor(); this.#hideDrawingCursor(); + this.#showDefaultCursor(); } else { - if (this.#state !== IDLE) this.#hideDefaultCursor(); + /* Hide all the unnecessary cursors depending on the active state */ + if (this.#state !== IDLE && this.#state !== COALITIONAREA_INTERACT) this.#hideDefaultCursor(); if (this.#state !== MOVE_UNIT) this.#hideDestinationCursors(); if (![BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) this.#hideTargetCursor(); - if (this.#state !== DRAW_COALITIONAREA_POLYGON) this.#hideDrawingCursor(); + if (this.#state !== COALITIONAREA_DRAW_POLYGON) this.#hideDrawingCursor(); - if (this.#state === IDLE) this.#showDefaultCursor(); + /* Show the active cursor depending on the active state */ + if (this.#state === IDLE || this.#state === COALITIONAREA_INTERACT) this.#showDefaultCursor(); else if (this.#state === MOVE_UNIT) this.#showDestinationCursors(); else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) this.#showTargetCursor(); - else if (this.#state === DRAW_COALITIONAREA_POLYGON) { - if (this.getSelectedCoalitionArea()?.getEditing()) - { - this.#hideDefaultCursor(); - this.#showDrawingCursor(); - } - else { - this.#hideDrawingCursor(); - this.#showDefaultCursor(); - } - } + else if (this.#state === COALITIONAREA_DRAW_POLYGON) this.#showDrawingCursor(); } } -} +} diff --git a/client/src/map/temporaryunitmarker.ts b/client/src/map/temporaryunitmarker.ts index 904ee83d..7c266e0e 100644 --- a/client/src/map/temporaryunitmarker.ts +++ b/client/src/map/temporaryunitmarker.ts @@ -1,13 +1,52 @@ -import { Icon } from "leaflet"; import { CustomMarker } from "./custommarker"; +import { SpawnOptions } from "../controls/mapcontextmenu"; +import { DivIcon } from "leaflet"; +import { SVGInjector } from "@tanem/svg-injector"; +import { getMarkerCategoryByName, getUnitDatabaseByCategory } from "../other/utils"; export class TemporaryUnitMarker extends CustomMarker { + #spawnOptions: SpawnOptions; + + constructor(spawnOptions: SpawnOptions) { + super(spawnOptions.latlng, {interactive: false}); + this.#spawnOptions = spawnOptions; + } + createIcon() { - var icon = new Icon({ - iconUrl: '/resources/theme/images/markers/temporary-icon.png', - iconSize: [52, 52], - iconAnchor: [26, 26] + const category = getMarkerCategoryByName(this.#spawnOptions.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.#spawnOptions.coalition); + + // Main icon + var unitIcon = document.createElement("div"); + unitIcon.classList.add("unit-icon"); + var img = document.createElement("img"); + img.src = `/resources/theme/images/units/${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 = getUnitDatabaseByCategory(category)?.getByName(this.#spawnOptions.name)?.shortLabel || ""; + el.append(shortLabel); + } + + this.getElement()?.appendChild(el); + this.getElement()?.classList.add("ol-temporary-marker"); } } \ No newline at end of file diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index 5b9419fe..a38e0f26 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -1,6 +1,9 @@ import { LatLng, Point, Polygon } from "leaflet"; import * as turf from "@turf/turf"; import { UnitDatabase } from "../units/unitdatabase"; +import { aircraftDatabase } from "../units/aircraftdatabase"; +import { helicopterDatabase } from "../units/helicopterdatabase"; +import { groundUnitsDatabase } from "../units/groundunitsdatabase"; export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) { const φ1 = deg2rad(lat1); // φ, λ in radians @@ -219,4 +222,29 @@ export function randomUnitBlueprintByRole(unitDatabse: UnitDatabase, role: strin const unitBlueprints = unitDatabse.getByRole(role); 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 (groundUnitsDatabase.getByName(name) != null){ + // TODO this is very messy + var role = groundUnitsDatabase.getByName(name)?.loadouts[0].roles[0]; + return (role?.includes("SAM")) ? "groundunit-sam" : "groundunit-other"; + } + else + return ""; // TODO add other unit types +} + +export function getUnitDatabaseByCategory(category: string) { + if (category == "aircraft") + return aircraftDatabase; + else if (category == "helicopter") + return helicopterDatabase; + else if (category.includes("groundunit")) + return groundUnitsDatabase; + else + return null; } \ No newline at end of file diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 646b78f1..fd71dba2 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -134,13 +134,13 @@ export function spawnExplosion(intensity: number, latlng: LatLng) { } export function spawnGroundUnit(spawnOptions: SpawnOptions) { - var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "immediate": spawnOptions.immediate? true: false }; + var command = { "type": spawnOptions.name, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "immediate": spawnOptions.immediate? true: false }; var data = { "spawnGround": command } POST(data, () => { }); } export function spawnAircraft(spawnOptions: SpawnOptions) { - var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "altitude": spawnOptions.altitude, "payloadName": spawnOptions.loadout != null ? spawnOptions.loadout : "", "airbaseName": spawnOptions.airbaseName != null ? spawnOptions.airbaseName : "", "immediate": spawnOptions.immediate? true: false }; + var command = { "type": spawnOptions.name, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "altitude": spawnOptions.altitude, "payloadName": spawnOptions.loadout != null ? spawnOptions.loadout : "", "airbaseName": spawnOptions.airbaseName != null ? spawnOptions.airbaseName : "", "immediate": spawnOptions.immediate? true: false }; var data = { "spawnAir": command } POST(data, () => { }); } diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index 54e9d37b..200c19a6 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -1,14 +1,12 @@ import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map } from 'leaflet'; import { getMap, getUnitsManager } from '..'; -import { mToFt, msToKnots, rad2deg } from '../other/utils'; +import { getMarkerCategoryByName, getUnitDatabaseByCategory, mToFt, msToKnots, rad2deg } from '../other/utils'; import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures, setSpeedType, setAltitudeType, setOnOff, setFollowRoads, bombPoint, carpetBomb, bombBuilding, fireAtArea } from '../server/server'; -import { aircraftDatabase } from './aircraftdatabase'; -import { groundUnitsDatabase } from './groundunitsdatabase'; import { CustomMarker } from '../map/custommarker'; import { SVGInjector } from '@tanem/svg-injector'; import { UnitDatabase } from './unitdatabase'; -import { BOMBING, CARPET_BOMBING, FIRE_AT_AREA, IDLE, MOVE_UNIT } from '../map/map'; import { TargetMarker } from '../map/targetmarker'; +import { BOMBING, CARPET_BOMBING, FIRE_AT_AREA, IDLE, MOVE_UNIT } from '../constants/constants'; var pathIcon = new Icon({ iconUrl: '/resources/theme/images/markers/marker-icon.png', @@ -133,12 +131,12 @@ export class Unit extends CustomMarker { getMarkerCategory() { // Overloaded by child classes + // TODO convert to use getMarkerCategoryByName return ""; } getDatabase(): UnitDatabase | null { - // Overloaded by child classes - return null; + return getUnitDatabaseByCategory(this.getMarkerCategory()); } getIconOptions(): UnitIconOptions { @@ -344,7 +342,7 @@ export class Unit extends CustomMarker { if (this.getIconOptions().showShortLabel) { var shortLabel = document.createElement("div"); shortLabel.classList.add("unit-short-label"); - shortLabel.innerText = this.getDatabase()?.getByName(this.getBaseData().name)?.shortLabel || ""; + shortLabel.innerText = getUnitDatabaseByCategory(this.getMarkerCategory())?.getByName(this.getBaseData().name)?.shortLabel || ""; el.append(shortLabel); } @@ -424,7 +422,7 @@ export class Unit extends CustomMarker { return getUnitsManager().getUnitByID(this.getFormationData().leaderID); } - canRole(roles: string | string[]) { + canFulfillRole(roles: string | string[]) { if (typeof(roles) === "string") roles = [roles]; @@ -565,7 +563,9 @@ export class Unit extends CustomMarker { /***********************************************/ onAdd(map: Map): this { super.onAdd(map); - getMap().removeTemporaryMarker(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); + /* If this is the first time adding this unit to the map, remove the temporary marker */ + if (getUnitsManager().getUnitByID(this.ID) == null) + getMap().removeTemporaryMarker(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); return this; } @@ -609,14 +609,14 @@ export class Unit extends CustomMarker { if ((selectedUnits.length === 0 && this.getBaseData().category == "Aircraft") || (selectedUnitTypes.length === 1 && ["Aircraft"].includes(selectedUnitTypes[0]))) { - if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canRole(["CAS", "Strike"])})) { + if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canFulfillRole(["CAS", "Strike"])})) { options["bomb"] = {text: "Precision bombing", tooltip: "Precision bombing of a specific point"}; options["carpet-bomb"] = {text: "Carpet bombing", tooltip: "Carpet bombing close to a point"}; } } if ((selectedUnits.length === 0 && this.getBaseData().category == "GroundUnit") || selectedUnitTypes.length === 1 && ["GroundUnit"].includes(selectedUnitTypes[0])) { - if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canRole(["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank"])})) + if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canFulfillRole(["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank"])})) options["fire-at-area"] = {text: "Fire at area", tooltip: "Fire at a large area"}; } @@ -915,21 +915,17 @@ export class AirUnit extends Unit { } export class Aircraft extends AirUnit { - constructor(ID: number, data: UnitData) { + constructor(ID: number, data: UpdateData) { super(ID, data); } getMarkerCategory() { return "aircraft"; } - - getDatabase(): UnitDatabase | null { - return aircraftDatabase; - } } export class Helicopter extends AirUnit { - constructor(ID: number, data: UnitData) { + constructor(ID: number, data: UpdateData) { super(ID, data); } @@ -939,7 +935,7 @@ export class Helicopter extends AirUnit { } export class GroundUnit extends Unit { - constructor(ID: number, data: UnitData) { + constructor(ID: number, data: UpdateData) { super(ID, data); } @@ -958,19 +954,12 @@ export class GroundUnit extends Unit { } getMarkerCategory() { - // TODO this is very messy - var role = groundUnitsDatabase.getByName(this.getBaseData().name)?.loadouts[0].roles[0]; - var markerCategory = (role === "SAM") ? "groundunit-sam" : "groundunit-other"; - return markerCategory; - } - - getDatabase(): UnitDatabase | null { - return groundUnitsDatabase; + return getMarkerCategoryByName(this.getBaseData().name); } } export class NavyUnit extends Unit { - constructor(ID: number, data: UnitData) { + constructor(ID: number, data: UpdateData) { super(ID, data); } @@ -994,7 +983,7 @@ export class NavyUnit extends Unit { } export class Weapon extends Unit { - constructor(ID: number, data: UnitData) { + constructor(ID: number, data: UpdateData) { super(ID, data); this.setSelectable(false); } @@ -1015,7 +1004,7 @@ export class Weapon extends Unit { } export class Missile extends Weapon { - constructor(ID: number, data: UnitData) { + constructor(ID: number, data: UpdateData) { super(ID, data); } @@ -1025,7 +1014,7 @@ export class Missile extends Weapon { } export class Bomb extends Weapon { - constructor(ID: number, data: UnitData) { + constructor(ID: number, data: UpdateData) { super(ID, data); } diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index f30e25d9..b45cf120 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -3,10 +3,10 @@ import { getHotgroupPanel, getInfoPopup, getMap, getMissionHandler, getUnitDataT import { Unit } from "./unit"; import { cloneUnit, spawnGroundUnit } from "../server/server"; import { deg2rad, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polygonArea, randomPointInPoly, randomUnitBlueprintByRole } from "../other/utils"; -import { IDLE, MOVE_UNIT } from "../map/map"; import { CoalitionArea } from "../map/coalitionarea"; import { Airbase } from "../missionhandler/airbase"; import { groundUnitsDatabase } from "./groundunitsdatabase"; +import { IDLE, MOVE_UNIT } from "../constants/constants"; export class UnitsManager { #units: { [ID: number]: Unit }; @@ -493,7 +493,7 @@ export class UnitsManager { if (!this.#pasteDisabled) { for (let idx in this.#copiedUnits) { var unit = this.#copiedUnits[idx]; - getMap().addTemporaryMarker(getMap().getMouseCoordinates()); + //getMap().addTemporaryMarker(getMap().getMouseCoordinates()); cloneUnit(unit.ID, getMap().getMouseCoordinates()); this.#showActionMessage(this.#copiedUnits, `pasted`); } @@ -520,14 +520,9 @@ export class UnitsManager { if (Math.random() < probability){ const role = activeRoles[Math.floor(Math.random() * activeRoles.length)]; const unitBlueprint = randomUnitBlueprintByRole(groundUnitsDatabase, role); - spawnGroundUnit({ - role: role, - latlng: latlng, - type: unitBlueprint.name, - coalition: coalitionArea.getCoalition(), - immediate: true - }); - getMap().addTemporaryMarker(latlng); + const spawnOptions = {role: role, latlng: latlng, name: unitBlueprint.name, coalition: coalitionArea.getCoalition(), immediate: true}; + spawnGroundUnit(spawnOptions); + getMap().addTemporaryMarker(spawnOptions); } } } diff --git a/client/views/other/contextmenus.ejs b/client/views/other/contextmenus.ejs index f1f1dc7c..eed3f719 100644 --- a/client/views/other/contextmenus.ejs +++ b/client/views/other/contextmenus.ejs @@ -116,6 +116,8 @@ data-on-click-params='{ "type": "iads" }' class="ol-contexmenu-button"> + diff --git a/client/views/panels/navbar.ejs b/client/views/panels/navbar.ejs index a2ca6b2e..5fa3e35f 100644 --- a/client/views/panels/navbar.ejs +++ b/client/views/panels/navbar.ejs @@ -61,6 +61,9 @@
+