feat: completed measure handling

This commit is contained in:
Davide Passoni
2025-02-03 17:27:19 +01:00
parent 627c4b5584
commit 52606b8d57
24 changed files with 869 additions and 75 deletions

View File

@@ -3,7 +3,7 @@ import { getApp } from "../olympusapp";
import { BoxSelect } from "./boxselect";
import { Airbase } from "../mission/airbase";
import { Unit } from "../unit/unit";
import { areaContains, bearing, bearingAndDistanceToLatLng, deepCopyTable, deg2rad, getGroundElevation, mToFt, mToNm, nmToM, rad2deg } from "../other/utils";
import { areaContains, deepCopyTable, deg2rad, getGroundElevation } from "../other/utils";
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
import { ClickableMiniMap } from "./clickableminimap";
import {
@@ -67,6 +67,9 @@ import {
import { ContextActionSet } from "../unit/contextactionset";
import { SmokeMarker } from "./markers/smokemarker";
import { MeasureMarker } from "./markers/measuremarker";
import { MeasureStartMarker } from "./markers/measurestartmarker";
import { MeasureEndMarker } from "./markers/measureendmarker";
import { Measure } from "./measure";
/* Register the handler for the box selection */
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
@@ -107,6 +110,7 @@ export class Map extends L.Map {
#isDragging: boolean = false;
#isSelecting: boolean = false;
#originalMouseClickLatLng: L.LatLng | null = null;
#debounceTimeout: number | null = null;
#isLeftMouseDown: boolean = false;
#isRightMouseDown: boolean = false;
@@ -157,9 +161,11 @@ export class Map extends L.Map {
#IPToTargetLine: L.Polygon | null = null;
/* Measure tool */
#measureReference: L.LatLng | null = null;
#measureLines: L.Polyline[] = [];
#measureMarkers: MeasureMarker[] = [];
#measures: Measure[] = [];
/* State variables */
#previousAppState: OlympusState = OlympusState.IDLE;
#previousAppSubstate: OlympusSubState = NO_SUBSTATE;
/**
*
@@ -338,6 +344,15 @@ export class Map extends L.Map {
shiftKey: false,
altKey: false,
ctrlKey: false,
}).addShortcut("clearMeasures", {
label: "Clear measures",
keyUpCallback: () => {
this.clearMeasures();
},
code: "KeyComma",
shiftKey: false,
altKey: false,
ctrlKey: false,
})
.addShortcut("toggleUnitLabels", {
label: "Hide/show labels",
@@ -854,7 +869,24 @@ 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`);
this.getContainer().classList.remove(`measure-cursor`);
/* Clear the last measure if the state is changed */
if (this.#previousAppState === OlympusState.MEASURE) {
if (this.#measures.length > 0 && this.#measures[this.#measures.length - 1].isActive()) {
this.#measures[this.#measures.length - 1].remove();
this.#measures.pop();
if (this.#measures.length > 0) {
if (this.#measures[this.#measures.length - 1].getDistance() < 1) {
this.#measures[this.#measures.length - 1].remove();
this.#measures.pop();
} else {
this.#measures[this.#measures.length - 1].showEndMarker();
}
}
}
}
/* Operations to perform when entering a state */
if (state === OlympusState.IDLE) {
getApp().getUnitsManager()?.deselectAllUnits();
@@ -883,7 +915,12 @@ export class Map extends L.Map {
console.log(this.#contextAction);
} else if (state === OlympusState.DRAW) {
if (subState === DrawSubState.DRAW_CIRCLE || subState === DrawSubState.DRAW_POLYGON) this.getContainer().classList.add(`plus-cursor`);
} else if (state === OlympusState.MEASURE) {
this.getContainer().classList.add(`measure-cursor`);
}
this.#previousAppState = state;
this.#previousAppSubstate = subState;
}
#onDragStart(e: any) {
@@ -918,18 +955,10 @@ export class Map extends L.Map {
}
#onMouseDown(e: any) {
this.#originalMouseClickLatLng = e.latlng;
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();
@@ -947,8 +976,6 @@ export class Map extends L.Map {
}
#onLeftShortClick(e: L.LeafletMouseEvent) {
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
if (Date.now() - this.#leftMouseDownEpoch < SHORT_PRESS_MILLISECONDS) {
this.#debounceTimeout = window.setTimeout(() => {
if (!this.#isSelecting) {
@@ -1060,12 +1087,25 @@ export class Map extends L.Map {
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 */
const newMeasure = new Measure(this);
const previousMeasure = this.#measures[this.#measures.length - 1];
this.#measures.push(newMeasure);
newMeasure.onClick(e.latlng);
if (previousMeasure && previousMeasure.isActive()) {
previousMeasure.finish();
previousMeasure.hideEndMarker();
newMeasure.onMarkerMoved = (startLatLng, endLatLng) => {
previousMeasure.moveMarkers(null, startLatLng);
};
}
} else {
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
else getApp().setState(OlympusState.UNIT_CONTROL);
}
}
if (this.#debounceTimeout) window.clearTimeout(this.#debounceTimeout);
this.#debounceTimeout = null;
}, DEBOUNCE_MILLISECONDS);
}
}
@@ -1128,18 +1168,13 @@ export class Map extends L.Map {
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}`);
if (this.#debounceTimeout === null) {
this.#measures[this.#measures.length - 1]?.onMouseMove(e.latlng);
let totalLength = 0;
this.#measures.forEach((measure) => {
measure.setTotalDistance(totalLength);
totalLength += measure.getDistance();
});
}
}
} else {
@@ -1208,11 +1243,6 @@ 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);
@@ -1294,4 +1324,9 @@ export class Map extends L.Map {
});
}
}
clearMeasures() {
this.#measures.forEach((measure) => measure.remove());
this.#measures = [];
}
}

View File

@@ -0,0 +1,57 @@
import { DivIcon, LatLngExpression, Map, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
import { SVGInjector } from "@tanem/svg-injector";
export class MeasureEndMarker extends CustomMarker {
#rotationAngle: number = 0;
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.options.interactive = true;
this.options.draggable = true;
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(
new DivIcon({
iconSize: [32, 32],
iconAnchor: [16, 16],
className: "leaflet-measure-end-marker",
})
);
var el = document.createElement("div");
el.classList.add("ol-measure-end-icon");
var img = document.createElement("img");
img.src = "images/markers/measure-end.svg";
img.onload = () => {
SVGInjector(img);
this.#applyRotation();
}
el.appendChild(img);
this.getElement()?.appendChild(el);
}
setRotationAngle(angle: number) {
this.#rotationAngle = angle;
this.#applyRotation();
}
getRotationAngle() {
return this.#rotationAngle;
}
onAdd(map: Map): this {
super.onAdd(map);
this.#applyRotation();
return this;
}
#applyRotation() {
const element = this.getElement();
if (element) {
const svg = element.querySelector("svg");
if (svg) svg.style.transform = `rotate(${this.#rotationAngle - Math.PI / 2}rad)`;
}
}
}

View File

@@ -2,9 +2,7 @@ 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 = () => {};
@@ -56,9 +54,7 @@ export class MeasureMarker extends Marker {
*/
setRotationAngle(angle: number) {
this.#rotationAngle = angle;
if (!this.#isEditable) {
this.#updateRotation();
}
this.#updateRotation();
}
/**

View File

@@ -0,0 +1,30 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
import { SVGInjector } from "@tanem/svg-injector";
export class MeasureStartMarker extends CustomMarker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.options.interactive = true;
this.options.draggable = true;
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(
new DivIcon({
iconSize: [32, 32],
iconAnchor: [16, 16],
className: "leaflet-measure-start-marker",
})
);
var el = document.createElement("div");
el.classList.add("ol-measure-start-icon");
var img = document.createElement("img");
img.src = "images/markers/measure-start.svg";
img.onload = () => SVGInjector(img);
el.appendChild(img);
this.getElement()?.appendChild(el);
}
}

View File

@@ -26,7 +26,7 @@ export class SmokeMarker extends CustomMarker {
);
var el = document.createElement("div");
el.classList.add("ol-smoke-icon");
el.setAttribute("data-color", this.#color);
el.style.fill = this.#color;
var img = document.createElement("img");
img.src = "images/markers/smoke.svg";
img.onload = () => SVGInjector(img);

View File

@@ -6,6 +6,7 @@
position: relative;
width: 40px;
height: 40px;
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
}
.airbase-icon svg {

View File

@@ -7,6 +7,7 @@
width: 100%;
height: 100%;
fill: white;
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
}
.bullseye-icon[data-coalition="red"] svg * {

View File

@@ -4,23 +4,36 @@
display: flex !important;
justify-content: center;
align-items: center;
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
width: fit-content !important;
height: fit-content !important;
margin: 0 !important;
translate: -50% -50%;
}
/* Container for the measure marker content */
.leaflet-measure-marker .container {
min-width: 150px;
transform-origin: center;
background-color: var(--background-steel);
color: white;
background-color: white;
color: #272727;
border-radius: 999px;
font-size: 13px;
align-content: center;
border: 2px solid transparent;
padding: 4px;
}
/* Text inside the measure marker */
.leaflet-measure-marker .text {
margin-left: 12px;
display: block;
margin-top: auto;
margin-bottom: auto;
font-weight: bolder;
text-wrap: nowrap;
padding-left: 5px;
padding-right: 5px;
}
.leaflet-measure-start-marker, .leaflet-measure-end-marker {
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
}

View File

@@ -6,6 +6,7 @@
justify-content: center;
position: relative;
width: 100%;
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
}
[data-object|="unit"].attack-cursor {

View File

@@ -0,0 +1,118 @@
import { LatLng, LeafletMouseEvent, Polyline } from "leaflet";
import { Map } from "./map";
import { MeasureMarker } from "./markers/measuremarker";
import { MeasureStartMarker } from "./markers/measurestartmarker";
import { MeasureEndMarker } from "./markers/measureendmarker";
import { bearing, deg2rad, midpoint, mToFt, mToNm, nmToM, rad2deg } from "../other/utils";
import { AppStateChangedEvent } from "../events";
import { OlympusState } from "../constants/constants";
export class Measure {
#active: boolean = false;
#map: Map;
#line: Polyline;
#measureMarker: MeasureMarker;
#startMarker: MeasureStartMarker;
#endMarker: MeasureEndMarker;
#totalDistance: number = 0;
onMarkerMoved: (startLatLng: LatLng, endLatLng: LatLng) => void = () => {};
constructor(map) {
this.#map = map;
}
onClick(latlng: LatLng) {
if (this.#startMarker === undefined) {
this.#startMarker = new MeasureStartMarker(latlng).addTo(this.#map);
this.#endMarker = new MeasureEndMarker(latlng).addTo(this.#map);
this.#line = new Polyline([this.#startMarker.getLatLng(), this.#endMarker.getLatLng()], { color: "#FFFFFF", dashArray: "5, 5" }).addTo(this.#map);
this.#measureMarker = new MeasureMarker(new LatLng(0, 0), "", 0).addTo(this.#map);
this.#startMarker.on("drag", (event) => {
this.#onMarkersMove();
});
this.#endMarker.on("drag", (event) => {
this.#onMarkersMove();
});
this.#active = true;
}
}
onMouseMove(latlng: LatLng) {
if (this.#endMarker !== undefined && this.isActive()) {
this.#endMarker.setLatLng(latlng);
this.#onMarkersMove();
}
}
remove() {
if (this.#startMarker !== undefined) this.#map.removeLayer(this.#startMarker);
if (this.#endMarker !== undefined) this.#map.removeLayer(this.#endMarker);
if (this.#line !== undefined) this.#map.removeLayer(this.#line);
if (this.#measureMarker !== undefined) this.#map.removeLayer(this.#measureMarker);
}
hideEndMarker() {
if (this.#endMarker !== undefined) this.#map.removeLayer(this.#endMarker);
}
showEndMarker() {
this.#onMarkersMove();
if (this.#endMarker !== undefined) this.#endMarker.addTo(this.#map);
}
moveMarkers(startLatLng: LatLng | null, endLatLng: LatLng | null) {
startLatLng && this.#startMarker.setLatLng(startLatLng);
endLatLng && this.#endMarker.setLatLng(endLatLng);
this.#onMarkersMove();
}
getDistance() {
return this.#startMarker.getLatLng().distanceTo(this.#endMarker.getLatLng());
}
finish() {
this.#active = false;
}
isActive() {
return this.#active;
}
setTotalDistance(distance: number) {
this.#totalDistance = distance;
}
#onMarkersMove() {
const distance = this.#startMarker.getLatLng().distanceTo(this.#endMarker.getLatLng());
let distanceString = "";
if (distance > nmToM(1)) distanceString = `${mToNm(distance).toFixed(distance < nmToM(10) ? 2 : 0)} NM`;
else distanceString = `${mToFt(distance).toFixed(0)} ft`;
const bearingTo = deg2rad(
bearing(this.#startMarker.getLatLng().lat, this.#startMarker.getLatLng().lng, this.#endMarker.getLatLng().lat, this.#endMarker.getLatLng().lng, false)
);
if (this.#totalDistance > 0) {
if (this.#totalDistance + this.getDistance() > nmToM(1)) distanceString += ` / ${mToNm(this.#totalDistance + this.getDistance()).toFixed(0)} NM`;
else distanceString += ` / ${mToFt(this.#totalDistance + this.getDistance()).toFixed(0)} ft`;
}
const halfPoint = midpoint(
this.#startMarker.getLatLng().lat,
this.#startMarker.getLatLng().lng,
this.#endMarker.getLatLng().lat,
this.#endMarker.getLatLng().lng
);
const bearingString = `${Math.floor(rad2deg(bearingTo) + 360) % 360}°`;
this.#measureMarker.setLatLng(halfPoint);
this.#measureMarker.setRotationAngle(bearingTo + Math.PI / 2);
this.#measureMarker.setTextValue(`${distanceString} - ${bearingString}`);
this.#endMarker.setRotationAngle(bearingTo);
this.#line.setLatLngs([this.#startMarker.getLatLng(), this.#endMarker.getLatLng()]);
this.onMarkerMoved(this.#startMarker.getLatLng(), this.#endMarker.getLatLng());
}
}

View File

@@ -139,12 +139,15 @@
background-image: url("/images/markers/target.svg");
height: 100%;
width: 100%;
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .2));
}
.ol-spot-icon {
background-image: url("/images/markers/target.svg");
height: 100%;
width: 100%;
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .2));
}
.ol-text-icon {
@@ -159,26 +162,7 @@
.ol-smoke-icon {
opacity: 75%;
}
[data-color="white"].ol-smoke-icon {
fill: white;
}
[data-color="blue"].ol-smoke-icon {
fill: blue;
}
[data-color="red"].ol-smoke-icon {
fill: red;
}
[data-color="green"].ol-smoke-icon {
fill: green;
}
[data-color="orange"].ol-smoke-icon {
fill: orange;
filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .2));
}
.ol-explosion-icon * {
@@ -225,6 +209,11 @@ path.leaflet-interactive:focus {
cursor: url("/images/cursors/plus.svg"), auto !important;
}
.measure-cursor {
cursor: url("/images/cursors/measure.svg"), auto !important;
}
#map-container.leaflet-grab {
cursor: url("/images/cursors/grab.svg") 16 16, auto;
}