From 86e0e6b3a3bbe6e982a2786f9c367036bd64b9f4 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Thu, 1 Aug 2024 09:02:21 +0200 Subject: [PATCH] Started adding controls panel for shortcuts, more work on better polygon creation, added circles creation --- frontend/react/package.json | 1 + frontend/react/src/constants/constants.ts | 1 + frontend/react/src/dom.d.ts | 4 +- .../src/map/coalitionarea/coalitionarea.ts | 5 + .../src/map/coalitionarea/coalitioncircle.ts | 162 ++++++++++++ .../src/map/coalitionarea/coalitionpolygon.ts | 60 +++-- .../src/map/coalitionarea/drawingcursor.ts | 22 -- frontend/react/src/map/map.ts | 226 ++++++++++++---- frontend/react/src/other/utils.ts | 29 ++- .../react/src/ui/components/oldropdown.tsx | 5 +- frontend/react/src/ui/panels/controls.tsx | 50 ++++ frontend/react/src/ui/panels/drawingmenu.tsx | 246 ++++++++++++++---- frontend/react/src/ui/ui.tsx | 2 + frontend/react/src/unit/unitsmanager.ts | 12 +- 14 files changed, 675 insertions(+), 150 deletions(-) create mode 100644 frontend/react/src/map/coalitionarea/coalitionarea.ts create mode 100644 frontend/react/src/map/coalitionarea/coalitioncircle.ts delete mode 100644 frontend/react/src/map/coalitionarea/drawingcursor.ts create mode 100644 frontend/react/src/ui/panels/controls.tsx diff --git a/frontend/react/package.json b/frontend/react/package.json index bd80335f..ca6df087 100644 --- a/frontend/react/package.json +++ b/frontend/react/package.json @@ -22,6 +22,7 @@ "js-sha256": "^0.11.0", "leaflet": "^1.9.4", "leaflet-control-mini-map": "^0.4.0", + "leaflet-path-drag": "^1.9.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 1f556893..1b769e00 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -260,6 +260,7 @@ export const IDLE = "Idle"; export const SPAWN_UNIT = "Spawn unit"; export const CONTEXT_ACTION = "Context action"; export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area polygon"; +export const COALITIONAREA_DRAW_CIRCLE = "Draw Coalition Area circle"; export const COALITIONAREA_EDIT = "Edit Coalition Area"; export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"]; diff --git a/frontend/react/src/dom.d.ts b/frontend/react/src/dom.d.ts index 146d437f..90d7c7af 100644 --- a/frontend/react/src/dom.d.ts +++ b/frontend/react/src/dom.d.ts @@ -1,4 +1,5 @@ import { ServerStatus } from "./interfaces"; +import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { Unit } from "./unit/unit"; interface CustomEventMap { @@ -20,7 +21,8 @@ interface CustomEventMap { contactsUpdated: CustomEvent; activeCoalitionChanged: CustomEvent; serverStatusUpdated: CustomEvent; - mapForceBoxSelect: CustomEvent + mapForceBoxSelect: CustomEvent; + coalitionAreaSelected: CustomEvent; } declare global { diff --git a/frontend/react/src/map/coalitionarea/coalitionarea.ts b/frontend/react/src/map/coalitionarea/coalitionarea.ts new file mode 100644 index 00000000..d057bcaa --- /dev/null +++ b/frontend/react/src/map/coalitionarea/coalitionarea.ts @@ -0,0 +1,5 @@ +const CoalitionArea = Base => class extends Base { + +} + +export CoalitionArea; \ No newline at end of file diff --git a/frontend/react/src/map/coalitionarea/coalitioncircle.ts b/frontend/react/src/map/coalitionarea/coalitioncircle.ts new file mode 100644 index 00000000..590d30b8 --- /dev/null +++ b/frontend/react/src/map/coalitionarea/coalitioncircle.ts @@ -0,0 +1,162 @@ +import { + LatLngExpression, + Map, + Circle, + DivIcon, + Marker, + CircleOptions, + LatLng, +} from "leaflet"; +import { getApp } from "../../olympusapp"; +import { CoalitionAreaHandle } from "./coalitionareahandle"; +import { BLUE_COMMANDER, RED_COMMANDER } from "../../constants/constants"; +import { Coalition } from "../../types/types"; +import * as turf from "@turf/turf"; + +let totalAreas = 0; + +export class CoalitionCircle extends Circle { + #coalition: Coalition = "blue"; + #selected: boolean = true; + #editing: boolean = true; + #radiusHandle: CoalitionAreaHandle; + #labelText: string; + #label: Marker; + + constructor(latlng: LatLngExpression, options: CircleOptions) { + if (options === undefined) options = { radius: 0 }; + + totalAreas++; + + options.bubblingMouseEvents = false; + options.interactive = false; + //@ts-ignore draggable option added by leaflet-path-drag + options.draggable = true; + + super(latlng, options); + this.#setColors(); + + this.#labelText = `Circle ${totalAreas}`; + + if ( + [BLUE_COMMANDER, RED_COMMANDER].includes( + getApp().getMissionManager().getCommandModeOptions().commandMode + ) + ) + this.setCoalition(getApp().getMissionManager().getCommandedCoalition()); + + this.on("drag", () => { + this.#setRadiusHandle(); + this.#drawLabel(); + }); + } + + setCoalition(coalition: Coalition) { + this.#coalition = coalition; + this.#setColors(); + } + + getCoalition() { + return this.#coalition; + } + + setSelected(selected: boolean) { + this.#selected = selected; + this.#setColors(); + this.#setRadiusHandle(); + this.#drawLabel(); + this.setOpacity(selected ? 1 : 0.5); + + //@ts-ignore draggable option added by leaflet-path-drag + selected ? this.dragging.enable() : this.dragging.disable(); + } + + getSelected() { + return this.#selected; + } + + setEditing(editing: boolean) { + this.#editing = editing; + this.#setRadiusHandle(); + } + + getEditing() { + return this.#editing; + } + + setOpacity(opacity: number) { + this.setStyle({ opacity: opacity, fillOpacity: opacity * 0.25 }); + } + + getLabelText() { + return this.#labelText; + } + + setLabelText(labelText: string) { + this.#labelText = labelText; + this.#drawLabel(); + } + + onRemove(map: Map): this { + super.onRemove(map); + this.#radiusHandle.removeFrom(getApp().getMap()); + return this; + } + + setLatLng(latlng: LatLngExpression): this { + super.setLatLng(latlng); + this.#setRadiusHandle(); + this.#drawLabel(); + return this; + } + + #setColors() { + let coalitionColor = "#FFFFFF"; + if (this.getCoalition() === "blue") coalitionColor = "#247be2"; + else if (this.getCoalition() === "red") coalitionColor = "#ff5858"; + + this.setStyle({ + color: this.getSelected() ? "white" : coalitionColor, + fillColor: coalitionColor, + }); + } + + #setRadiusHandle() { + if (this.#radiusHandle) this.#radiusHandle.removeFrom(getApp().getMap()); + + if (this.#selected) { + const dest = turf.destination( + turf.point([this.getLatLng().lng, this.getLatLng().lat]), + this.getRadius() / 1000, + 0 + ); + this.#radiusHandle = new CoalitionAreaHandle( + new LatLng(dest.geometry.coordinates[1], dest.geometry.coordinates[0]) + ); + this.#radiusHandle.addTo(getApp().getMap()); + this.#radiusHandle.on("drag", (e: any) => { + this.setRadius(this.getLatLng().distanceTo(e.latlng)); + }); + } + } + + #drawLabel() { + if (this.#label) { + this.#label.removeFrom(this._map); + } + this.#label = new Marker(this.getLatLng(), { + icon: new DivIcon({ + className: "label", + html: this.#labelText, + iconSize: [100, 40], + }), + interactive: false, + }).addTo(this._map); + this.#label + .getElement() + ?.classList.add( + `ol-coalitionarea-label`, + `${this.#selected ? "selected" : `${this.#coalition}`}` + ); + } +} diff --git a/frontend/react/src/map/coalitionarea/coalitionpolygon.ts b/frontend/react/src/map/coalitionarea/coalitionpolygon.ts index 4f20c766..81f1912f 100644 --- a/frontend/react/src/map/coalitionarea/coalitionpolygon.ts +++ b/frontend/react/src/map/coalitionarea/coalitionpolygon.ts @@ -6,7 +6,7 @@ import { Polygon, PolylineOptions, DivIcon, - Marker + Marker, } from "leaflet"; import { getApp } from "../../olympusapp"; import { CoalitionAreaHandle } from "./coalitionareahandle"; @@ -37,11 +37,13 @@ export class CoalitionPolygon extends Polygon { options.bubblingMouseEvents = false; options.interactive = false; + //@ts-ignore draggable option added by leaflet-path-drag + options.draggable = true; super(latlngs, options); this.#setColors(); - this.#labelText = `Polygon ${totalAreas}` + this.#labelText = `Polygon ${totalAreas}`; if ( [BLUE_COMMANDER, RED_COMMANDER].includes( @@ -49,6 +51,12 @@ export class CoalitionPolygon extends Polygon { ) ) this.setCoalition(getApp().getMissionManager().getCommandedCoalition()); + + this.on("drag", () => { + this.#setHandles(); + this.#setMiddleHandles(); + this.#drawLabel(); + }); } setCoalition(coalition: Coalition) { @@ -73,6 +81,9 @@ export class CoalitionPolygon extends Polygon { this.setLatLngs(latlngs); this.setEditing(false); } + + //@ts-ignore draggable option added by leaflet-path-drag + selected ? this.dragging.enable() : this.dragging.disable(); } getSelected() { @@ -103,7 +114,7 @@ export class CoalitionPolygon extends Polygon { setOpacity(opacity: number) { this.setStyle({ opacity: opacity, fillOpacity: opacity * 0.25 }); } - + getLabelText() { return this.#labelText; } @@ -123,15 +134,19 @@ export class CoalitionPolygon extends Polygon { return this; } - setLatLngs(latlngs: LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][]){ + setLatLngs( + latlngs: LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][] + ) { super.setLatLngs(latlngs); this.#drawLabel(); return this; } #setColors() { - const coalitionColor = - this.getCoalition() === "blue" ? "#247be2" : "#ff5858"; + let coalitionColor = "#FFFFFF"; + if (this.getCoalition() === "blue") coalitionColor = "#247be2"; + else if (this.getCoalition() === "red") coalitionColor = "#ff5858"; + this.setStyle({ color: this.getSelected() ? "white" : coalitionColor, fillColor: coalitionColor, @@ -196,18 +211,25 @@ export class CoalitionPolygon extends Polygon { } } - #drawLabel() { - if (this.#label) { - this.#label.removeFrom(this._map); + #drawLabel() { + if (this.#label) { + this.#label.removeFrom(this._map); + } + if ((this.getLatLngs()[0] as LatLng[]).length > 2) { + this.#label = new Marker(polyCenter(this), { + icon: new DivIcon({ + className: "label", + html: this.#labelText, + iconSize: [100, 40], + }), + interactive: false, + }).addTo(this._map); + this.#label + .getElement() + ?.classList.add( + `ol-coalitionarea-label`, + `${this.#selected ? "selected" : `${this.#coalition}`}` + ); + } } - this.#label = new Marker(polyCenter(this), { - icon: new DivIcon({ - className: 'label', - html: this.#labelText, - iconSize: [100, 40] - }), - interactive: false - }).addTo(this._map); - this.#label.getElement()?.classList.add(`ol-coalitionarea-label`, `${this.#selected? "selected": `${this.#coalition}`}`); - } } diff --git a/frontend/react/src/map/coalitionarea/drawingcursor.ts b/frontend/react/src/map/coalitionarea/drawingcursor.ts deleted file mode 100644 index a783e622..00000000 --- a/frontend/react/src/map/coalitionarea/drawingcursor.ts +++ /dev/null @@ -1,22 +0,0 @@ -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); - } -} diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 9ab49843..83b66e71 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -3,7 +3,13 @@ import { getApp } from "../olympusapp"; import { BoxSelect } from "./boxselect"; import { Airbase } from "../mission/airbase"; import { Unit } from "../unit/unit"; -import { deg2rad, getGroundElevation, polyContains } from "../other/utils"; +import { + areaContains, + circleContains, + deg2rad, + getGroundElevation, + polyContains, +} from "../other/utils"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { @@ -18,6 +24,7 @@ import { MAP_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, COALITIONAREA_EDIT, + COALITIONAREA_DRAW_CIRCLE, } from "../constants/constants"; import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon"; import { MapHiddenTypes, MapOptions } from "../types/types"; @@ -29,6 +36,10 @@ import "./markers/stylesheets/airbase.css"; import "./markers/stylesheets/bullseye.css"; import "./markers/stylesheets/units.css"; import "./map.css"; +import { CoalitionCircle } from "./coalitionarea/coalitioncircle"; + +import "leaflet-path-drag"; +import { faLeftLong } from "@fortawesome/free-solid-svg-icons"; /* Register the handler for the box selection */ L.Map.addInitHook("addHandler", "boxSelect", BoxSelect); @@ -92,7 +103,7 @@ export class Map extends L.Map { #cameraZoomRatio: number = 1.0; /* Coalition areas drawing */ - #coalitionPolygons: CoalitionPolygon[] = []; + #coalitionAreas: (CoalitionPolygon | CoalitionCircle)[] = []; /* Unit context actions */ #contextAction: null | ContextAction = null; @@ -132,7 +143,7 @@ export class Map extends L.Map { this.#miniMapPolyline.addTo(this.#miniMapLayerGroup); /* Init the state machine */ - this.#state = IDLE; + this.setState(IDLE); /* Register event handles */ this.on("zoomstart", (e: any) => this.#onZoomStart(e)); @@ -174,18 +185,6 @@ export class Map extends L.Map { ); }); - document.addEventListener( - "toggleCoalitionAreaDraw", - (ev: CustomEventInit) => { - 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); @@ -366,27 +365,38 @@ export class Map extends L.Map { console.log(`Switching from state ${this.#state} to ${state}`); /* Operations to perform when leaving a state */ - if (this.#state === COALITIONAREA_DRAW_POLYGON) { + if ( + this.#state === COALITIONAREA_DRAW_POLYGON || + this.#state === COALITIONAREA_DRAW_CIRCLE + ) this.getSelectedCoalitionArea()?.setEditing(false); - } this.#state = state; /* Operations to perform when entering a state */ if (this.#state === IDLE) { - getApp().getUnitsManager().deselectAllUnits(); + getApp().getUnitsManager()?.deselectAllUnits(); this.deselectAllCoalitionAreas(); } else if (this.#state === SPAWN_UNIT) { + this.deselectAllCoalitionAreas(); this.#spawnRequestTable = options?.spawnRequestTable ?? null; console.log(`Spawn request table:`); console.log(this.#spawnRequestTable); } else if (this.#state === CONTEXT_ACTION) { + this.deselectAllCoalitionAreas(); this.#contextAction = options?.contextAction ?? null; console.log(`Context action:`); console.log(this.#contextAction); } else if (this.#state === COALITIONAREA_DRAW_POLYGON) { - this.#coalitionPolygons.push(new CoalitionPolygon([])); - this.#coalitionPolygons[this.#coalitionPolygons.length - 1].addTo(this); + getApp().getUnitsManager().deselectAllUnits(); + this.#coalitionAreas.push(new CoalitionPolygon([])); + this.#coalitionAreas[this.#coalitionAreas.length - 1].addTo(this); + } else if (this.#state === COALITIONAREA_DRAW_CIRCLE) { + getApp().getUnitsManager().deselectAllUnits(); + this.#coalitionAreas.push( + new CoalitionCircle(new L.LatLng(0, 0), { radius: 1000 }) + ); + this.#coalitionAreas[this.#coalitionAreas.length - 1].addTo(this); } document.dispatchEvent( @@ -398,18 +408,122 @@ export class Map extends L.Map { return this.#state; } + getCurrentControls() { + if (this.#state === IDLE) { + return [ + { + actions: ["Tap"], + text: "Select unit", + }, + { + actions: ["Shift", "Drag"], + text: "Box selection", + }, + { + actions: ["Drag"], + text: "Move map location", + }, + { + actions: ["Long tap", "Drag"], + text: "Box selection", + }, + ]; + } else if (this.#state === SPAWN_UNIT) { + return [ + { + actions: ["Tap"], + text: "Spawn unit", + }, + { + actions: ["Double tap"], + text: "Exit spawn mode", + }, + { + actions: ["Drag"], + text: "Move map location", + }, + ]; + } else if (this.#state === CONTEXT_ACTION) { + return [ + { + actions: ["Tap"], + text: this.#contextAction?.getLabel() ?? "", + }, + { + actions: ["Double tap"], + text: "Exit action mode", + }, + { + actions: ["Drag"], + text: "Move map location", + }, + ]; + } else if (this.#state === COALITIONAREA_EDIT) { + return [ + { + actions: ["Tap"], + text: "Select shape", + }, + { + actions: ["Double tap"], + text: "Exit drawing mode", + }, + { + actions: ["Drag"], + text: "Move map location", + }, + ]; + } else if (this.#state === COALITIONAREA_DRAW_POLYGON) { + return [ + { + actions: ["Tap"], + text: "Add vertex to polygon", + }, + { + actions: ["Double tap"], + text: "Finalize polygon", + }, + { + actions: ["Drag"], + text: "Move map location", + }, + ]; + } else if (this.#state === COALITIONAREA_DRAW_CIRCLE) { + return [ + { + actions: ["Tap"], + text: "Add circle", + }, + { + actions: ["Drag"], + text: "Move map location", + }, + ]; + } else { + return []; + } + } + deselectAllCoalitionAreas() { - this.#coalitionPolygons.forEach((coalitionPolygon: CoalitionPolygon) => - coalitionPolygon.setSelected(false) + document.dispatchEvent( + new CustomEvent("coalitionAreaSelected", { + detail: null, + }) + ); + + this.#coalitionAreas.forEach( + (coalitionArea: CoalitionPolygon | CoalitionCircle) => + coalitionArea.setSelected(false) ); } - deleteCoalitionArea(coalitionArea: CoalitionPolygon) { - if (this.#coalitionPolygons.includes(coalitionArea)) - this.#coalitionPolygons.splice( - this.#coalitionPolygons.indexOf(coalitionArea), + deleteCoalitionArea(coalitionArea: CoalitionPolygon | CoalitionCircle) { + if (this.#coalitionAreas.includes(coalitionArea)) + this.#coalitionAreas.splice( + this.#coalitionAreas.indexOf(coalitionArea), 1 ); + if (this.hasLayer(coalitionArea)) this.removeLayer(coalitionArea); } @@ -550,15 +664,13 @@ export class Map extends L.Map { } getSelectedCoalitionArea() { - return this.#coalitionPolygons.find((coalitionPolygon: CoalitionPolygon) => { - return coalitionPolygon.getSelected(); - }); - } + const coalitionArea = this.#coalitionAreas.find( + (coalitionArea: CoalitionPolygon | CoalitionCircle) => { + return coalitionArea.getSelected(); + } + ); - bringCoalitionAreaToBack(coalitionArea: CoalitionPolygon) { - coalitionArea.bringToBack(); - this.#coalitionPolygons.splice(this.#coalitionPolygons.indexOf(coalitionArea), 1); - this.#coalitionPolygons.unshift(coalitionArea); + return coalitionArea ?? null; } setOption(key, value) { @@ -644,7 +756,7 @@ export class Map extends L.Map { } preventClicks() { - console.log("Preventing clicks on map") + console.log("Preventing clicks on map"); window.clearTimeout(this.#shortPressTimer); window.clearTimeout(this.#longPressTimer); } @@ -699,7 +811,10 @@ export class Map extends L.Map { window.clearTimeout(this.#shortPressTimer); window.clearTimeout(this.#longPressTimer); - if (this.#state === COALITIONAREA_DRAW_POLYGON) { + if ( + this.#state === COALITIONAREA_DRAW_POLYGON || + this.#state === COALITIONAREA_DRAW_CIRCLE + ) { this.setState(COALITIONAREA_EDIT); } else { this.setState(IDLE); @@ -736,11 +851,33 @@ export class Map extends L.Map { ); } } else if (this.#state === COALITIONAREA_DRAW_POLYGON) { - this.getSelectedCoalitionArea()?.getEditing() - ? this.getSelectedCoalitionArea()?.addTemporaryLatLng(location) - : this.deselectAllCoalitionAreas(); - } else if (this.#state === COALITIONAREA_EDIT) { + const selectedArea = this.getSelectedCoalitionArea(); + if (selectedArea && selectedArea instanceof CoalitionPolygon) { + selectedArea.addTemporaryLatLng(location); + } + } else if (this.#state === COALITIONAREA_DRAW_CIRCLE) { + const selectedArea = this.getSelectedCoalitionArea(); + if (selectedArea && selectedArea instanceof CoalitionCircle) { + if ( + selectedArea.getLatLng().lat == 0 && + selectedArea.getLatLng().lng == 0 + ) + selectedArea.setLatLng(location); + this.setState(COALITIONAREA_EDIT); + } + } else if (this.#state == COALITIONAREA_EDIT) { this.deselectAllCoalitionAreas(); + for (let idx = 0; idx < this.#coalitionAreas.length; idx++) { + if (areaContains(e.latlng, this.#coalitionAreas[idx])) { + this.#coalitionAreas[idx].setSelected(true); + document.dispatchEvent( + new CustomEvent("coalitionAreaSelected", { + detail: this.#coalitionAreas[idx], + }) + ); + break; + } + } } else if (this.#state === CONTEXT_ACTION) { this.executeContextAction(null, e.latlng); } else { @@ -750,6 +887,8 @@ export class Map extends L.Map { #onLongPress(e: any) { console.log(`Long press at ${e.latlng}`); + this.deselectAllCoalitionAreas(); + if (!this.#isDragging && !this.#isZooming) { if (this.#state == IDLE) { if (e.type === "touchstart") @@ -760,13 +899,6 @@ export class Map extends L.Map { document.dispatchEvent( new CustomEvent("mapForceBoxSelect", { detail: e.originalEvent }) ); - } else if (this.#state == COALITIONAREA_EDIT) { - for (let idx = 0; idx < this.#coalitionPolygons.length; idx++) { - if (polyContains(e.latlng, this.#coalitionPolygons[idx])) { - this.#coalitionPolygons[idx].setSelected(true); - break; - } - } } } } diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 05cc5e02..850d157d 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -1,4 +1,4 @@ -import { LatLng, Polygon } from "leaflet"; +import { Circle, LatLng, Polygon } from "leaflet"; import * as turf from "@turf/turf"; import { UnitDatabase } from "../unit/databases/unitdatabase"; import { @@ -17,6 +17,7 @@ import { DateAndTime, UnitBlueprint } from "../interfaces"; import { Converter } from "usng"; import { MGRS } from "../types/types"; import { getApp } from "../olympusapp"; +import { featureCollection } from "turf"; export function bearing( lat1: number, @@ -315,14 +316,30 @@ export function nmToFt(nm: number) { return nm * 6076.12; } +export function areaContains(latlng: LatLng, area: Polygon | Circle) { + if (area instanceof Polygon) + return polyContains(latlng, area); + else + return circleContains(latlng, area); +} + export function polyContains(latlng: LatLng, polygon: Polygon) { - const poly = polygon.toGeoJSON(); + let coordinates = [(polygon.getLatLngs()[0] as LatLng[]).map((latlng) => {return [latlng.lng, latlng.lat]} )]; + coordinates[0].push([polygon.getLatLngs()[0][0].lng, polygon.getLatLngs()[0][0].lat]) + const poly = turf.polygon(coordinates); + return turf.inside(turf.point([latlng.lng, latlng.lat]), poly); +} + +export function circleContains(latlng: LatLng, circle: Circle) { + const poly = turf.circle(turf.point([circle.getLatLng().lng, circle.getLatLng().lat]), circle.getRadius() / 1000, 100, 'kilometers'); return turf.inside(turf.point([latlng.lng, latlng.lat]), poly); } export function polyCenter(polygon: Polygon) { - const poly = polygon.toGeoJSON(); - const center = turf.center(poly); + let coordinates = [(polygon.getLatLngs()[0] as LatLng[]).map((latlng) => {return [latlng.lng, latlng.lat]} )]; + coordinates[0].push([polygon.getLatLngs()[0][0].lng, polygon.getLatLngs()[0][0].lat]) + const poly = turf.polygon(coordinates); + const center = turf.center(featureCollection([poly])); return new LatLng(center.geometry.coordinates[1], center.geometry.coordinates[0]); } @@ -337,7 +354,9 @@ export function randomPointInPoly(polygon: Polygon): LatLng { var lat = y_min + Math.random() * (y_max - y_min); var lng = x_min + Math.random() * (x_max - x_min); - var poly = polygon.toGeoJSON(); + let coordinates = [(polygon.getLatLngs()[0] as LatLng[]).map((latlng) => {return [latlng.lng, latlng.lat]} )]; + coordinates[0].push([polygon.getLatLngs()[0][0].lng, polygon.getLatLngs()[0][0].lat]) + const poly = turf.polygon(coordinates); var inside = turf.inside(turf.point([lng, lat]), poly); if (inside) { diff --git a/frontend/react/src/ui/components/oldropdown.tsx b/frontend/react/src/ui/components/oldropdown.tsx index defd78ba..a55b7336 100644 --- a/frontend/react/src/ui/components/oldropdown.tsx +++ b/frontend/react/src/ui/components/oldropdown.tsx @@ -84,9 +84,8 @@ export function OlDropdown(props: { const target = event.target; if ( target && - /*!content.contains(target as HTMLElement) &&*/ !button.contains( - target as HTMLElement - ) + !content.contains(target as HTMLElement) && + !button.contains(target as HTMLElement) ) { setOpen(false); } diff --git a/frontend/react/src/ui/panels/controls.tsx b/frontend/react/src/ui/panels/controls.tsx new file mode 100644 index 00000000..e8899222 --- /dev/null +++ b/frontend/react/src/ui/panels/controls.tsx @@ -0,0 +1,50 @@ +import React, { useState } from "react"; +import { getApp } from "../../olympusapp"; +export function ControlsPanel(props: {}) { + const [controls, setControls] = useState( + [] as { actions: string[]; text: string }[] + ); + document.addEventListener("mapStateChanged", (ev) => { + setControls(getApp().getMap().getCurrentControls()); + }); + + return ( +
+ {controls.map((control) => { + return ( +
+
{control.text}
+
+ {control.actions.map((action, idx) => { + return ( + <> +
+ {action} +
+ {idx !== control.actions.length - 1 &&
+
} + + ); + })} +
+
+ ); + })} +
+ ); +} diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index 856baaca..2fc60648 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -3,6 +3,7 @@ import { Menu } from "./components/menu"; import { FaQuestionCircle, FaRegCircle } from "react-icons/fa"; import { getApp } from "../../olympusapp"; import { + COALITIONAREA_DRAW_CIRCLE, COALITIONAREA_DRAW_POLYGON, COALITIONAREA_EDIT, IDLE, @@ -16,28 +17,44 @@ import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; import { OlCheckbox } from "../components/olcheckbox"; import { Coalition } from "../../types/types"; import { OlRangeSlider } from "../components/olrangeslider"; +import { CoalitionCircle } from "../../map/coalitionarea/coalitioncircle"; export function DrawingMenu(props: { open: boolean; onClose: () => void }) { const [drawingPolygon, setDrawingPolygon] = useState(false); const [drawingCircle, setDrawingCircle] = useState(false); const [activeCoalitionArea, setActiveCoalitionArea] = useState( - null as null | CoalitionPolygon + null as null | CoalitionPolygon | CoalitionCircle ); const [areaCoalition, setAreaCoalition] = useState("blue" as Coalition); const [IADSDensity, setIADSDensity] = useState(50); const [IADSDistribution, setIADSDistribution] = useState(50); - const [forceCoalitionAppropriateUnits, setForceCoalitionApproriateUnits] = useState(false); + const [forceCoalitionAppropriateUnits, setForceCoalitionApproriateUnits] = + useState(false); + + const [typesSelection, setTypesSelection] = useState({}); + const [erasSelection, setErasSelection] = useState({}); + const [rangesSelection, setRangesSelection] = useState({}); useEffect(() => { + /* If we are not in polygon drawing mode, force the draw polygon button off */ if ( drawingPolygon && getApp().getMap().getState() !== COALITIONAREA_DRAW_POLYGON ) setDrawingPolygon(false); - if (props.open && !drawingPolygon) + /* If we are not in circle drawing mode, force the draw circle button off */ + if ( + drawingCircle && + getApp().getMap().getState() !== COALITIONAREA_DRAW_CIRCLE + ) + setDrawingCircle(false); + + /* If we are not in any drawing mode, force the map in edit mode */ + if (props.open && !drawingPolygon && !drawingCircle) getApp().getMap().setState(COALITIONAREA_EDIT); + /* Align the state of the coalition toggle to the coalition of the area */ if ( activeCoalitionArea && activeCoalitionArea?.getCoalition() !== areaCoalition @@ -52,42 +69,91 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { ) setDrawingPolygon(false); - if ( - [COALITIONAREA_DRAW_POLYGON, COALITIONAREA_EDIT].includes( - getApp().getMap().getState() - ) - ) { + if (getApp().getMap().getState() == COALITIONAREA_EDIT) { setActiveCoalitionArea( getApp().getMap().getSelectedCoalitionArea() ?? null ); } }); + document.addEventListener("coalitionAreaSelected", (event: any) => { + setActiveCoalitionArea(event.detail); + }); + return ( { + setActiveCoalitionArea(null); + getApp().getMap().deselectAllCoalitionAreas(); + }} > -
- The draw tool allows you to quickly draw areas on the map and use these - areas to spawn units and activate triggers. -
-
-
- -
-
-
- Use the polygon or paint tool to draw areas on the map. + <> + {activeCoalitionArea === null && !drawingPolygon && !drawingCircle && ( + <> +
+ The draw tool allows you to quickly draw areas on the map and use + these areas to spawn units and activate triggers. +
+
+
+ +
+
+
+ Use the polygon or circle tool to draw areas on the map. +
+
+ After drawing a shape, select it to see the options for + spawning units. Click on a shape to select it. +
+
+
+ + )} + + + <> + {activeCoalitionArea === null && drawingPolygon && ( +
+
+ +
+
+
+ Click on the map to add vertices to the polygon. +
+
+ When you are done, double click on the map to finalize the + polygon. Vertices can be dragged or added to adjust the shape. +
+
-
- After drawing a shape, select it to see the options for spawning - units. + )} + + + <> + {activeCoalitionArea === null && drawingCircle && ( +
+
+ +
+
+
+ Click on the map to add a new circle. +
+
+ You can drag the circle to move it and you can use the handle to set the radius. +
+
-
-
+ )} + + <> {activeCoalitionArea === null && (
@@ -110,9 +176,14 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { icon={faCircle} tooltip={"Add a new circle"} checked={drawingCircle} - onClick={() => {}} + onClick={() => { + if (drawingCircle) + getApp().getMap().setState(COALITIONAREA_EDIT); + else getApp().getMap().setState(COALITIONAREA_DRAW_CIRCLE); + setDrawingCircle(!drawingCircle); + }} > -
Add circle (WIP)
+
Add circle
)} @@ -137,7 +208,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { dark:focus:ring-blue-500 focus:border-blue-500 focus:ring-blue-500 `} - defaultValue={activeCoalitionArea.getLabelText()} + placeholder={activeCoalitionArea.getLabelText()} onInput={(ev) => activeCoalitionArea.setLabelText(ev.currentTarget.value) } @@ -174,11 +245,26 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { {getApp() .getGroundUnitDatabase() .getTypes() - .map((era) => { + .map((type) => { + if (!(type in typesSelection)) { + typesSelection[type] = true; + setTypesSelection( + JSON.parse(JSON.stringify(typesSelection)) + ); + } + return ( - {}} /> - {era} + { + typesSelection[type] = ev.currentTarget.checked; + setTypesSelection( + JSON.parse(JSON.stringify(typesSelection)) + ); + }} + /> + {type} ); })} @@ -188,20 +274,50 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { .getGroundUnitDatabase() .getEras() .map((era) => { + if (!(era in erasSelection)) { + erasSelection[era] = true; + setErasSelection( + JSON.parse(JSON.stringify(erasSelection)) + ); + } + return ( - {}} /> + { + erasSelection[era] = ev.currentTarget.checked; + setErasSelection( + JSON.parse(JSON.stringify(erasSelection)) + ); + }} + /> {era} ); })} - {["Short range", "Medium range", "Long range"].map((era) => { + {["Short range", "Medium range", "Long range"].map((range) => { + if (!(range in rangesSelection)) { + rangesSelection[range] = true; + setRangesSelection( + JSON.parse(JSON.stringify(rangesSelection)) + ); + } + return ( - {}} /> - {era} + { + rangesSelection[range] = ev.currentTarget.checked; + setErasSelection( + JSON.parse(JSON.stringify(rangesSelection)) + ); + }} + /> + {range} ); })} @@ -215,10 +331,15 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { dark:text-blue-500 `} > - 50% + {IADSDensity}%
- {}}> + { + setIADSDensity(Number(ev.currentTarget.value)); + }} + >
@@ -229,24 +350,53 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { dark:text-blue-500 `} > - 50% + {IADSDistribution}%
- {setIADSDistribution(Number(ev.target.value))}}> + { + setIADSDistribution(Number(ev.target.value)); + }} + >
- { - setForceCoalitionApproriateUnits(!forceCoalitionAppropriateUnits); - }} /> + { + setForceCoalitionApproriateUnits( + !forceCoalitionAppropriateUnits + ); + }} + /> Force coalition appropriate units
- + )} diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 25b19a85..c1a24e96 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -25,6 +25,7 @@ import { sha256 } from "js-sha256"; import { MiniMapPanel } from "./panels/minimappanel"; import { UnitMouseControlBar } from "./panels/unitmousecontrolbar"; import { DrawingMenu } from "./panels/drawingmenu"; +import { ControlsPanel } from "./panels/controls"; export type OlympusState = { mainMenuVisible: boolean; @@ -235,6 +236,7 @@ export function UI() { options={mapOptions} /> + setUnitControlMenuVisible(false)} diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 5fba4387..71e138fc 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -2,6 +2,7 @@ import { LatLng, LatLngBounds } from "leaflet"; import { getApp } from "../olympusapp"; import { Unit } from "./unit"; import { + areaContains, bearingAndDistanceToLatLng, deg2rad, getGroundElevation, @@ -44,6 +45,7 @@ import { import { Group } from "./group"; import { UnitDataFileExport } from "./importexport/unitdatafileexport"; import { UnitDataFileImport } from "./importexport/unitdatafileimport"; +import { CoalitionCircle } from "../map/coalitionarea/coalitioncircle"; /** 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 @@ -1642,7 +1644,7 @@ export class UnitsManager { * @param distribution Value between 0 and 100, controls how "scattered" the units will be */ createIADS( - coalitionArea: CoalitionPolygon, + coalitionArea: CoalitionPolygon | CoalitionCircle, types: { [key: string]: boolean }, eras: { [key: string]: boolean }, ranges: { [key: string]: boolean }, @@ -1665,7 +1667,7 @@ export class UnitsManager { var airbase = airbases[airbaseName]; /* Check if the city is inside the coalition area */ if ( - polyContains( + areaContains( new LatLng(airbase.getLatLng().lat, airbase.getLatLng().lng), coalitionArea ) @@ -1684,7 +1686,7 @@ export class UnitsManager { ); /* Make sure the unit is still inside the coalition area */ - if (polyContains(latlng, coalitionArea)) { + if (areaContains(latlng, coalitionArea)) { const type = activeTypes[Math.floor(Math.random() * activeTypes.length)]; if (Math.random() < IADSDensities[type]) { @@ -1729,7 +1731,7 @@ export class UnitsManager { 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)) { + if (areaContains(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++) { @@ -1744,7 +1746,7 @@ export class UnitsManager { ); /* Make sure the unit is still inside the coalition area */ - if (polyContains(latlng, coalitionArea)) { + if (areaContains(latlng, coalitionArea)) { const type = activeTypes[Math.floor(Math.random() * activeTypes.length)]; if (Math.random() < IADSDensities[type]) {