diff --git a/frontend/react/public/images/cursors/measure.svg b/frontend/react/public/images/cursors/measure.svg
new file mode 100644
index 00000000..5477c284
--- /dev/null
+++ b/frontend/react/public/images/cursors/measure.svg
@@ -0,0 +1,130 @@
+
+
diff --git a/frontend/react/public/images/markers/explosion.svg b/frontend/react/public/images/markers/explosion.svg
index 2f9b89e0..5048be60 100644
--- a/frontend/react/public/images/markers/explosion.svg
+++ b/frontend/react/public/images/markers/explosion.svg
@@ -26,8 +26,8 @@
width="52mm"
units="px"
inkscape:zoom="8.3856042"
- inkscape:cx="14.250613"
- inkscape:cy="36.550735"
+ inkscape:cx="-19.855457"
+ inkscape:cy="24.506284"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
@@ -46,14 +46,67 @@
offset="0"
id="stop4715" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/react/public/images/markers/marker-icon.png b/frontend/react/public/images/markers/marker-icon.png
deleted file mode 100644
index 950edf24..00000000
Binary files a/frontend/react/public/images/markers/marker-icon.png and /dev/null differ
diff --git a/frontend/react/public/images/markers/marker-shadow.png b/frontend/react/public/images/markers/marker-shadow.png
deleted file mode 100644
index 9fd29795..00000000
Binary files a/frontend/react/public/images/markers/marker-shadow.png and /dev/null differ
diff --git a/frontend/react/public/images/markers/measure-end.svg b/frontend/react/public/images/markers/measure-end.svg
new file mode 100644
index 00000000..1ce9e63e
--- /dev/null
+++ b/frontend/react/public/images/markers/measure-end.svg
@@ -0,0 +1,60 @@
+
+
+
+
diff --git a/frontend/react/public/images/markers/measure-start.svg b/frontend/react/public/images/markers/measure-start.svg
new file mode 100644
index 00000000..72d965c1
--- /dev/null
+++ b/frontend/react/public/images/markers/measure-start.svg
@@ -0,0 +1,62 @@
+
+
+
+
diff --git a/frontend/react/public/images/markers/path.svg b/frontend/react/public/images/markers/path.svg
new file mode 100644
index 00000000..61a883ff
--- /dev/null
+++ b/frontend/react/public/images/markers/path.svg
@@ -0,0 +1,114 @@
+
+
diff --git a/frontend/react/public/images/markers/pin.svg b/frontend/react/public/images/markers/pin.svg
new file mode 100644
index 00000000..1d4ec458
--- /dev/null
+++ b/frontend/react/public/images/markers/pin.svg
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts
index 2537db1b..a3764e8e 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, 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 = [];
+ }
}
diff --git a/frontend/react/src/map/markers/measureendmarker.ts b/frontend/react/src/map/markers/measureendmarker.ts
new file mode 100644
index 00000000..125d493a
--- /dev/null
+++ b/frontend/react/src/map/markers/measureendmarker.ts
@@ -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)`;
+ }
+ }
+}
diff --git a/frontend/react/src/map/markers/measuremarker.ts b/frontend/react/src/map/markers/measuremarker.ts
index 1cf34e90..9ce0eea3 100644
--- a/frontend/react/src/map/markers/measuremarker.ts
+++ b/frontend/react/src/map/markers/measuremarker.ts
@@ -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();
}
/**
diff --git a/frontend/react/src/map/markers/measurestartmarker.ts b/frontend/react/src/map/markers/measurestartmarker.ts
new file mode 100644
index 00000000..d1b1f0fc
--- /dev/null
+++ b/frontend/react/src/map/markers/measurestartmarker.ts
@@ -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);
+ }
+}
diff --git a/frontend/react/src/map/markers/smokemarker.ts b/frontend/react/src/map/markers/smokemarker.ts
index cc131620..b321d557 100644
--- a/frontend/react/src/map/markers/smokemarker.ts
+++ b/frontend/react/src/map/markers/smokemarker.ts
@@ -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);
diff --git a/frontend/react/src/map/markers/stylesheets/airbase.css b/frontend/react/src/map/markers/stylesheets/airbase.css
index e0ad9a75..826e46a4 100644
--- a/frontend/react/src/map/markers/stylesheets/airbase.css
+++ b/frontend/react/src/map/markers/stylesheets/airbase.css
@@ -6,6 +6,7 @@
position: relative;
width: 40px;
height: 40px;
+ filter: drop-shadow( 3px 3px 3px rgba(0, 0, 0, .4));
}
.airbase-icon svg {
diff --git a/frontend/react/src/map/markers/stylesheets/bullseye.css b/frontend/react/src/map/markers/stylesheets/bullseye.css
index 7d2896e8..ab3bff5d 100644
--- a/frontend/react/src/map/markers/stylesheets/bullseye.css
+++ b/frontend/react/src/map/markers/stylesheets/bullseye.css
@@ -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 * {
diff --git a/frontend/react/src/map/markers/stylesheets/measure.css b/frontend/react/src/map/markers/stylesheets/measure.css
index 412aa50e..b89b590b 100644
--- a/frontend/react/src/map/markers/stylesheets/measure.css
+++ b/frontend/react/src/map/markers/stylesheets/measure.css
@@ -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));
+}
\ No newline at end of file
diff --git a/frontend/react/src/map/markers/stylesheets/units.css b/frontend/react/src/map/markers/stylesheets/units.css
index 13d326d9..40085d15 100644
--- a/frontend/react/src/map/markers/stylesheets/units.css
+++ b/frontend/react/src/map/markers/stylesheets/units.css
@@ -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 {
diff --git a/frontend/react/src/map/measure.ts b/frontend/react/src/map/measure.ts
new file mode 100644
index 00000000..66d00c16
--- /dev/null
+++ b/frontend/react/src/map/measure.ts
@@ -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());
+ }
+}
diff --git a/frontend/react/src/map/stylesheets/map.css b/frontend/react/src/map/stylesheets/map.css
index 4cfd3751..6834f088 100644
--- a/frontend/react/src/map/stylesheets/map.css
+++ b/frontend/react/src/map/stylesheets/map.css
@@ -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;
}
diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts
index 58a680ce..ca35007c 100644
--- a/frontend/react/src/other/utils.ts
+++ b/frontend/react/src/other/utils.ts
@@ -46,6 +46,32 @@ export function bearingAndDistanceToLatLng(lat: number, lon: number, brng: numbe
return new LatLng(rad2deg(φ2), rad2deg(λ2));
}
+export function midpoint(lat1: number, lon1: number, lat2: number, lon2: number, zoom: number = 10) {
+ const φ1 = deg2rad(lat1); // Convert latitude of point 1 from degrees to radians
+ const λ1 = deg2rad(lon1); // Convert longitude of point 1 from degrees to radians
+ const φ2 = deg2rad(lat2); // Convert latitude of point 2 from degrees to radians
+ const λ2 = deg2rad(lon2); // Convert longitude of point 2 from degrees to radians
+
+ // Convert point 1 to Mercator projection coordinates
+ const x1 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI + λ1);
+ const y1 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI - Math.log(Math.tan(Math.PI / 4 + φ1 / 2)));
+
+ // Convert point 2 to Mercator projection coordinates
+ const x2 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI + λ2);
+ const y2 = 1 / (2 * Math.PI) * Math.pow(2, zoom) * (Math.PI - Math.log(Math.tan(Math.PI / 4 + φ2 / 2)));
+
+ // Calculate the midpoint in Mercator projection coordinates
+ const mx = (x1 + x2) / 2;
+ const my = (y1 + y2) / 2;
+
+ // Convert the midpoint back to latitude and longitude
+ const λ = (2 * Math.PI * mx / Math.pow(2, zoom)) - Math.PI;
+ const φ = 2 * Math.atan(Math.exp(Math.PI - (2 * Math.PI * my) / Math.pow(2, zoom))) - Math.PI / 2;
+
+ // Return the midpoint as a LatLng object
+ return new LatLng(rad2deg(φ), rad2deg(λ));
+}
+
export function ConvertDDToDMS(D: number, lng: boolean) {
var deg = 0 | (D < 0 ? (D = -D) : D);
var min = 0 | (((D += 1e-9) % 1) * 60);
@@ -93,6 +119,11 @@ export function latLngToMGRS(lat: number, lng: number, precision: number = 4): M
return undefined;
}
+ if (lng > 360 || lng < -180 || lat > 84 || lat < -80) {
+ console.error("latLngToMGRS: value outside of bounds");
+ return undefined;
+ }
+
const mgrs = new Converter({}).LLtoMGRS(lat, lng, precision);
const match = mgrs.match(new RegExp(`^(\\d{2})([A-Z])([A-Z])([A-Z])(\\d+)$`));
if (match) {
diff --git a/frontend/react/src/ui/panels/effectspawnmenu.tsx b/frontend/react/src/ui/panels/effectspawnmenu.tsx
index 2e2d0388..b86fb956 100644
--- a/frontend/react/src/ui/panels/effectspawnmenu.tsx
+++ b/frontend/react/src/ui/panels/effectspawnmenu.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
import { getApp } from "../../olympusapp";
-import { NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
+import { colors, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
import { OlStateButton } from "../components/olstatebutton";
import { faArrowLeft, faSmog } from "@fortawesome/free-solid-svg-icons";
import { LatLng } from "leaflet";
@@ -97,7 +97,7 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
Smoke color
- {["white", "blue", "red", "green", "orange"].map((optionSmokeColor) => {
+ {[colors.WHITE, colors.BLUE, colors.RED, colors.GREEN, colors.ORANGE].map((optionSmokeColor) => {
return (
)}
+
+
(
+
+ {shortcutCombination(shortcuts["measure"]?.getOptions())}
+
Enter measure mode
+
+ )}
+ tooltipPosition="side"
+ onClick={() => {
+ getApp().setState(appState === OlympusState.MEASURE? OlympusState.IDLE : OlympusState.MEASURE);
+ }}
+ />
+
+
+
(
+
+ {shortcutCombination(shortcuts["clearMeasures"]?.getOptions())}
+
Clear all measures
+
+ )}
+ tooltipPosition="side"
+ onClick={() => {
+ getApp().getMap().clearMeasures();
+ }}
+ />
+
>
{reorderedActions.map((contextActionIt: ContextAction) => {
diff --git a/frontend/react/src/ui/panels/radiossummarypanel.tsx b/frontend/react/src/ui/panels/radiossummarypanel.tsx
index 34ff7991..e4324d97 100644
--- a/frontend/react/src/ui/panels/radiossummarypanel.tsx
+++ b/frontend/react/src/ui/panels/radiossummarypanel.tsx
@@ -5,6 +5,7 @@ import { RadioSink } from "../../audio/radiosink";
import { FaJetFighter, FaRadio, FaVolumeHigh } from "react-icons/fa6";
import { OlStateButton } from "../components/olstatebutton";
import { UnitSink } from "../../audio/unitsink";
+import { colors } from "../../constants/constants";
export function RadiosSummaryPanel(props: {}) {
const [audioSinks, setAudioSinks] = useState([] as AudioSink[]);
@@ -41,7 +42,7 @@ export function RadiosSummaryPanel(props: {}) {
radioSink.setPtt(false);
}}
tooltip="Click to talk, lights up when receiving"
- buttonColor={radioSink.getReceiving() ? "white" : null}
+ buttonColor={radioSink.getReceiving() ? colors.WHITE : undefined}
className="min-h-12 min-w-12"
>