From 52606b8d577193241e31931b20658c416be18ff3 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Mon, 3 Feb 2025 17:27:19 +0100 Subject: [PATCH] feat: completed measure handling --- .../react/public/images/cursors/measure.svg | 130 ++++++++++++++++++ .../react/public/images/markers/explosion.svg | 59 +++++++- .../public/images/markers/marker-icon.png | Bin 1466 -> 0 bytes .../public/images/markers/marker-shadow.png | Bin 618 -> 0 bytes .../public/images/markers/measure-end.svg | 60 ++++++++ .../public/images/markers/measure-start.svg | 62 +++++++++ frontend/react/public/images/markers/path.svg | 114 +++++++++++++++ frontend/react/public/images/markers/pin.svg | 69 ++++++++++ frontend/react/src/map/map.ts | 103 +++++++++----- .../react/src/map/markers/measureendmarker.ts | 57 ++++++++ .../react/src/map/markers/measuremarker.ts | 6 +- .../src/map/markers/measurestartmarker.ts | 30 ++++ frontend/react/src/map/markers/smokemarker.ts | 2 +- .../src/map/markers/stylesheets/airbase.css | 1 + .../src/map/markers/stylesheets/bullseye.css | 1 + .../src/map/markers/stylesheets/measure.css | 21 ++- .../src/map/markers/stylesheets/units.css | 1 + frontend/react/src/map/measure.ts | 118 ++++++++++++++++ frontend/react/src/map/stylesheets/map.css | 29 ++-- frontend/react/src/other/utils.ts | 31 +++++ .../react/src/ui/panels/effectspawnmenu.tsx | 6 +- frontend/react/src/ui/panels/maptoolbar.tsx | 36 ++++- .../src/ui/panels/radiossummarypanel.tsx | 3 +- frontend/react/src/unit/unit.ts | 5 +- 24 files changed, 869 insertions(+), 75 deletions(-) create mode 100644 frontend/react/public/images/cursors/measure.svg delete mode 100644 frontend/react/public/images/markers/marker-icon.png delete mode 100644 frontend/react/public/images/markers/marker-shadow.png create mode 100644 frontend/react/public/images/markers/measure-end.svg create mode 100644 frontend/react/public/images/markers/measure-start.svg create mode 100644 frontend/react/public/images/markers/path.svg create mode 100644 frontend/react/public/images/markers/pin.svg create mode 100644 frontend/react/src/map/markers/measureendmarker.ts create mode 100644 frontend/react/src/map/markers/measurestartmarker.ts create mode 100644 frontend/react/src/map/measure.ts 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + 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 950edf24677ded147df13b26f91baa2b0fa70513..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1466 zcmV;r1x5OaP)P001cn1^@s6z>|W`000GnNklGNuHDcIX17Zdjl&3`L?0sTjIws<{((Dh&g-s0<@jYQyl?D*X^?%13;ml^gy> ziMrY_^1WI=(g@LMizu=zCoA>C`6|QEq1eV92k*7m>G65*&@&6)aC&e}G zI)pf-Za|N`DT&Cn1J|o`19mumxW~hiKiKyc-P`S@q)rdTo84@QI@;0yXrG%9uhI>A zG5QHb6s4=<6xy{1 z@NMxEkryp{LS44%z$3lP^cX!9+2-;CTt3wM4(k*#C{aiIiLuB>jJj;KPhPzIC00bL zU3a#;aJld94lCW=`4&aAy8M7PY=HQ>O%$YEP4c4UY#CRxfgbE~(|uiI=YS8q;O9y6 zmIkXzR`}p7ti|PrM3a}WMnR=3NVnWdAAR>b9X@)DKL6=YsvmH%?I24wdq?Gh54_;# z$?_LvgjEdspdQlft#4CQ z`2Zyvy?*)N1Ftw|{_hakhG9WjS?Az@I@+IZ8JbWewR!XUK4&6346+d#~gsE0SY(LX8&JfY>Aj)RxGy96nwhs2rv zzW6pTnMpFkDSkT*a*6Dx|u@ds6ISVn0@^RmIsKZ5Y;bazbc;tTSq(kg(=481ODrPyNB6n z-$+U}(w$m6U6H$w17Bw+wDaFIe~GvNMYvnw31MpY0eQKT9l>SU``8k7w4)z!GZKMI z#_cEKq7k~i%nlK@6c-K?+R;B#5$?T#YpKD`t_4bAs^#E+@5QW$@OX3*`;(#{U^d-vY)&xEE>n5lYl&T?Amke9$Lam@{1K@O ze*LXqlKQHiv=gx+V^Cbb2?z@ISBQ*3amF;9UJ3SBg(N|710TLamQmYZ&Qjn2LuO<* zCZlB4n%@pc&7NNnY1}x+NWpHlq`OJEo|`aYN9<`RBUB+79g;>dgb6YlfN#kGL?lO_ z!6~M^7sOnbsUkKk<@Ysie&`G>ruxH&Mgy&8;i=A zB9OO!xR{AyODw>DS-q5YM{0ExFEAzt zm>RdS+ssW(-8|?xr0(?$vBVB*%(xDLtq3Hf0I5yFm<_g=W2`QWAax{1rWVH=I!VrP zs(rTFX@W#t$hXNvbgX`gK&^w_YD;CQ!B@e0QbLIWaKAXQe2-kkloo;{iF#6}z!4=W zi$giRj1{ zt;2w`VSCF#WE&*ev7jpsC=6175@(~nTE2;7M-L((0bH@yG}-TB$R~WXd?tA$s3|%y zA`9$sA(>F%J3ioz<-LJl*^o1|w84l>HBR`>3l9c8$5Xr@xCiIQ7{x$fMCzOk_-M=% z+{a_Q#;42`#KfUte@$NT77uaTz?b-fBe)1s5XE$yA79fm?KqM^VgLXD07*qoM6N<$ Ef<_J(9smFU 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" >