From 627c4b5584523ad1a3f2a733af594209245a257f Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 31 Jan 2025 17:25:14 +0100 Subject: [PATCH] feat: started working at measuring tool --- frontend/react/src/constants/constants.ts | 3 +- frontend/react/src/map/map.ts | 53 ++++++++++- .../react/src/map/markers/measuremarker.ts | 90 +++++++++++++++++++ .../src/map/markers/stylesheets/measure.css | 26 ++++++ 4 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 frontend/react/src/map/markers/measuremarker.ts create mode 100644 frontend/react/src/map/markers/stylesheets/measure.css diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 8a818a7e..2b55b5e5 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -325,7 +325,8 @@ export enum OlympusState { GAME_MASTER = "Game master", IMPORT_EXPORT = "Import/export", WARNING = "Warning modal", - DATABASE_EDITOR = "Database editor" + DATABASE_EDITOR = "Database editor", + MEASURE = "Measure" } export const NO_SUBSTATE = "No substate"; diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index c83a82f5..2537db1b 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -3,7 +3,7 @@ import { getApp } from "../olympusapp"; import { BoxSelect } from "./boxselect"; import { Airbase } from "../mission/airbase"; import { Unit } from "../unit/unit"; -import { areaContains, deepCopyTable, deg2rad, getGroundElevation } from "../other/utils"; +import { areaContains, bearing, bearingAndDistanceToLatLng, deepCopyTable, deg2rad, getGroundElevation, mToFt, mToNm, nmToM, rad2deg } from "../other/utils"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { @@ -36,6 +36,7 @@ import "./markers/stylesheets/airbase.css"; import "./markers/stylesheets/bullseye.css"; import "./markers/stylesheets/units.css"; import "./markers/stylesheets/spot.css"; +import "./markers/stylesheets/measure.css"; import "./stylesheets/map.css"; import { initDraggablePath } from "./coalitionarea/draggablepath"; @@ -65,11 +66,12 @@ import { } from "../events"; import { ContextActionSet } from "../unit/contextactionset"; import { SmokeMarker } from "./markers/smokemarker"; +import { MeasureMarker } from "./markers/measuremarker"; /* Register the handler for the box selection */ L.Map.addInitHook("addHandler", "boxSelect", BoxSelect); -initDraggablePath(L); +initDraggablePath(L); export class Map extends L.Map { /* Options */ @@ -154,6 +156,11 @@ export class Map extends L.Map { #targetPoint: TargetMarker | null = null; #IPToTargetLine: L.Polygon | null = null; + /* Measure tool */ + #measureReference: L.LatLng | null = null; + #measureLines: L.Polyline[] = []; + #measureMarkers: MeasureMarker[] = []; + /** * * @param ID - the ID of the HTML element which will contain the map @@ -322,6 +329,16 @@ export class Map extends L.Map { getApp() .getShortcutManager() + .addShortcut("measure", { + label: "Toggle measurement tool", + keyUpCallback: () => { + getApp().getState() === OlympusState.MEASURE ? getApp().setState(OlympusState.IDLE) : getApp().setState(OlympusState.MEASURE); + }, + code: "KeyM", + shiftKey: false, + altKey: false, + ctrlKey: false, + }) .addShortcut("toggleUnitLabels", { label: "Hide/show labels", keyUpCallback: () => this.setOption("showUnitLabels", !this.getOptions().showUnitLabels), @@ -837,7 +854,7 @@ export class Map extends L.Map { this.getContainer().classList.remove(`explosion-cursor`); ["white", "blue", "red", "green", "orange"].forEach((color) => this.getContainer().classList.remove(`smoke-${color}-cursor`)); this.getContainer().classList.remove(`plus-cursor`); - + /* Operations to perform when entering a state */ if (state === OlympusState.IDLE) { getApp().getUnitsManager()?.deselectAllUnits(); @@ -904,6 +921,15 @@ export class Map extends L.Map { if (e.originalEvent?.button === 0) { this.#isLeftMouseDown = true; this.#leftMouseDownEpoch = Date.now(); + + /* If we are in the measure state there can only be a short click so immediately perform the action */ + if (getApp().getState() === OlympusState.MEASURE) { + if (this.#measureLines.length > 0 && this.#measureReference) + this.#measureLines[this.#measureLines.length - 1].setLatLngs([this.#measureReference, e.latlng]); + this.#measureReference = e.latlng; + this.#measureLines.push(new L.Polyline([this.#measureReference, e.latlng], { color: "magenta" }).addTo(this)); + this.#measureMarkers.push(new MeasureMarker(e.latlng, "", 0).addTo(this)); + } } else if (e.originalEvent?.button === 2) { this.#isRightMouseDown = true; this.#rightMouseDownEpoch = Date.now(); @@ -1033,6 +1059,8 @@ export class Map extends L.Map { if (this.#contextAction !== null) this.executeContextAction(null, e.latlng, e.originalEvent); else if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE); else getApp().setState(OlympusState.UNIT_CONTROL); + } else if (getApp().getState() === OlympusState.MEASURE) { + /* Do nothing, we already clicked on the mouse down callback */ } else { if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE); else getApp().setState(OlympusState.UNIT_CONTROL); @@ -1099,6 +1127,20 @@ export class Map extends L.Map { if (getApp().getState() === OlympusState.SPAWN) { if (this.#currentSpawnMarker) this.#currentSpawnMarker.setLatLng(e.latlng); if (this.#currentEffectMarker) this.#currentEffectMarker.setLatLng(e.latlng); + } else if (getApp().getState() === OlympusState.MEASURE) { + if (this.#measureLines.length > 0) this.#measureLines[this.#measureLines.length - 1].setLatLngs([this.#measureReference, e.latlng]); + if (this.#measureMarkers.length > 0 && this.#measureReference) { + const distance = this.#measureReference.distanceTo(e.latlng); + let distanceString = "" + if (distance > nmToM(1)) distanceString = `${mToNm(distance).toFixed(2)} NM`; + else distanceString = `${mToFt(distance).toFixed(2)} ft`; + const bearingTo = deg2rad(bearing(this.#measureReference.lat, this.#measureReference.lng, e.latlng.lat, e.latlng.lng, false)); + const halfPoint = bearingAndDistanceToLatLng(this.#measureReference.lat, this.#measureReference.lng, bearingTo, distance/2); + const bearingString = `${(Math.floor(rad2deg(bearingTo) + 360) % 360)}°`; + this.#measureMarkers[this.#measureMarkers.length - 1].setLatLng(halfPoint); + this.#measureMarkers[this.#measureMarkers.length - 1].setRotationAngle(bearingTo + Math.PI / 2); + this.#measureMarkers[this.#measureMarkers.length - 1].setTextValue(`${distanceString} - ${bearingString}`); + } } } else { this.#destionationWasRotated = true; @@ -1166,6 +1208,11 @@ export class Map extends L.Map { }); } + #clearMeasures() { + this.#measureLines.forEach((line) => line.removeFrom(this)); + this.#measureMarkers.forEach((marker) => marker.removeFrom(this)); + } + /* */ #panToUnit(unit: Unit) { var unitPosition = new L.LatLng(unit.getPosition().lat, unit.getPosition().lng); diff --git a/frontend/react/src/map/markers/measuremarker.ts b/frontend/react/src/map/markers/measuremarker.ts new file mode 100644 index 00000000..1cf34e90 --- /dev/null +++ b/frontend/react/src/map/markers/measuremarker.ts @@ -0,0 +1,90 @@ +import { Marker, LatLng, DivIcon, Map } from "leaflet"; + +export class MeasureMarker extends Marker { + #textValue: string; + #isEditable: boolean = false; + #rotationAngle: number; // Rotation angle in radians + #previousValue: string; + + onValueUpdated: (value: number) => void = () => {}; + onDeleteButtonClicked: () => void = () => {}; + + /** + * Constructor for SpotEditMarker + * @param {LatLng} latlng - The geographical position of the marker. + * @param {string} textValue - The initial text value to display. + * @param {number} rotationAngle - The initial rotation angle in radians. + */ + constructor(latlng: LatLng, textValue: string, rotationAngle: number = 0) { + super(latlng, { + icon: new DivIcon({ + className: "leaflet-measure-marker", + html: `
+
${textValue}
+
`, + }), + }); + + this.#textValue = textValue; + this.#rotationAngle = rotationAngle; + } + + /** + * Sets the text value of the marker. + * @param {string} textValue - The new text value. + */ + setTextValue(textValue: string) { + this.#textValue = textValue; + const element = this.getElement(); + if (element) { + const text = element.querySelector(".text"); + if (text) text.textContent = textValue; + } + } + + /** + * Gets the text value of the marker. + * @returns {string} - The current text value. + */ + getTextValue() { + return this.#textValue; + } + + /** + * Sets the rotation angle of the marker in radians. + * @param {number} angle - The new rotation angle in radians. + */ + setRotationAngle(angle: number) { + this.#rotationAngle = angle; + if (!this.#isEditable) { + this.#updateRotation(); + } + } + + /** + * Gets the rotation angle of the marker in radians. + * @returns {number} - The current rotation angle in radians. + */ + getRotationAngle() { + return this.#rotationAngle; + } + + /** + * Updates the rotation angle to ensure the text is always readable. + */ + #updateRotation() { + const element = this.getElement(); + if (element) { + const container = element.querySelector(".container") as HTMLDivElement; + if (container) { + let angle = this.#rotationAngle % (2 * Math.PI); + if (angle < 0) angle += 2 * Math.PI; + if (angle > Math.PI / 2 && angle < (3 * Math.PI) / 2) { + angle = (angle + Math.PI) % (2 * Math.PI); // Flip the text to be upright + } + const angleInDegrees = angle * (180 / Math.PI); + container.style.transform = `rotate(${angleInDegrees}deg)`; + } + } + } +} diff --git a/frontend/react/src/map/markers/stylesheets/measure.css b/frontend/react/src/map/markers/stylesheets/measure.css new file mode 100644 index 00000000..412aa50e --- /dev/null +++ b/frontend/react/src/map/markers/stylesheets/measure.css @@ -0,0 +1,26 @@ +/* Container for the measure marker */ +.leaflet-measure-marker { + text-align: center; + display: flex !important; + justify-content: center; + align-items: center; +} + +/* Container for the measure marker content */ +.leaflet-measure-marker .container { + min-width: 150px; + transform-origin: center; + background-color: var(--background-steel); + color: white; + border-radius: 999px; + font-size: 13px; + align-content: center; + border: 2px solid transparent; +} + +/* Text inside the measure marker */ +.leaflet-measure-marker .text { + margin-left: 12px; + margin-top: auto; + margin-bottom: auto; +}