feat: started adding editable laser/infrared

This commit is contained in:
Davide Passoni
2025-01-29 17:25:09 +01:00
parent 74e2332b17
commit cc902aec04
16 changed files with 490 additions and 10 deletions

View File

@@ -35,6 +35,7 @@ import { ContextAction } from "../unit/contextaction";
import "./markers/stylesheets/airbase.css";
import "./markers/stylesheets/bullseye.css";
import "./markers/stylesheets/units.css";
import "./markers/stylesheets/spot.css";
import "./stylesheets/map.css";
import { initDraggablePath } from "./coalitionarea/draggablepath";

View File

@@ -0,0 +1,216 @@
import { Marker, LatLng, DivIcon, DomEvent, Map } from "leaflet";
export class SpotEditMarker extends Marker {
#textValue: string;
#isEditable: boolean = false;
#rotationAngle: number; // Rotation angle in radians
#previousValue: string;
constructor(latlng: LatLng, textValue: string, rotationAngle: number = 0) {
super(latlng, {
icon: new DivIcon({
className: "leaflet-spot-input-marker",
html: `<div class="container">
<input class="input"/>
<div class="text">${textValue}</div>
<div class="delete">X</div>
</div>`,
}),
});
this.#textValue = textValue;
this.#rotationAngle = rotationAngle;
this.#previousValue = textValue;
}
onAdd(map: Map): this {
super.onAdd(map);
const element = this.getElement();
if (element) {
const text = element.querySelector(".text");
const button = element.querySelector(".delete");
const input = element.querySelector(".input") as HTMLInputElement;
// Add click event listener to toggle edit mode
text?.addEventListener("mousedown", (ev) => this.#toggleEditMode(ev));
text?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
text?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
// Add click event listener to delete spot
button?.addEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
button?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
button?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
// Add click event listener to input spot
input?.addEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
input?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
input?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
input?.addEventListener("blur", (ev) => this.#toggleEditMode(ev));
input?.addEventListener("keydown", (ev) => this.#acceptInput(ev));
input?.addEventListener("input", (ev) => this.#validateInput(ev));
}
return this;
}
onRemove(map: Map): this {
super.onRemove(map);
const element = this.getElement();
if (element) {
const text = element.querySelector(".text");
const button = element.querySelector(".delete");
const input = element.querySelector(".input");
// Add click event listener to toggle edit mode
text?.removeEventListener("mousedown", (ev) => this.#toggleEditMode(ev));
text?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
text?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
// Add click event listener to delete spot
button?.removeEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
button?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
button?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
// Add click event listener to input spot
input?.removeEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
input?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
input?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
input?.removeEventListener("blur", (ev) => this.#toggleEditMode(ev));
input?.removeEventListener("keydown", (ev) => this.#acceptInput(ev));
input?.removeEventListener("input", (ev) => this.#validateInput(ev));
}
return this;
}
// Method to set the text value
setTextValue(textValue: string) {
this.#textValue = textValue;
const element = this.getElement();
if (element) {
const text = element.querySelector(".text");
if (text) text.textContent = textValue;
}
}
// Method to get the text value
getTextValue() {
return this.#textValue;
}
// Method to set the rotation angle in radians
setRotationAngle(angle: number) {
this.#rotationAngle = angle;
if (!this.#isEditable) {
this.#updateRotation();
}
}
// Method to get the rotation angle in radians
getRotationAngle() {
return this.#rotationAngle;
}
// Method to update 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)`;
}
}
}
#toggleEditMode(ev) {
this.#isEditable = !this.#isEditable;
ev.stopPropagation();
ev.preventDefault();
const element = this.getElement();
if (element) {
const textElement = element.querySelector(".text");
const inputElement = element.querySelector(".input") as HTMLInputElement;
const buttonElement = element.querySelector(".delete");
const container = element.querySelector(".container") as HTMLDivElement;
if (this.#isEditable) {
// Rotate to horizontal and make text editable
if (textElement && inputElement && buttonElement) {
textElement.classList.add("edit-mode");
inputElement.style.display = "block";
inputElement.value = this.#textValue;
inputElement.focus();
buttonElement.textContent = "✔"; // Change to check mark
buttonElement.classList.add("edit-mode");
}
container.style.transform = `rotate(0deg)`;
} else {
// Save the edited text and revert to normal mode
if (textElement && inputElement && buttonElement) {
const newText = inputElement.value || this.#textValue;
this.#textValue = newText;
textElement.classList.remove("edit-mode");
inputElement.style.display = "none";
buttonElement.textContent = "X"; // Change to delete mark
buttonElement.classList.remove("edit-mode");
}
this.#updateRotation();
}
}
}
#stopEventPropagation(ev) {
ev.stopPropagation();
}
#validateInput(ev) {
const element = this.getElement();
if (element) {
const input = ev.target as HTMLInputElement;
const text = element.querySelector(".text");
// Validate the input value
const value = input.value;
// Check if the value is a partial valid input
const isPartialValid = /^[1]$|^[1][1-7]$|^[1][1-7][1-8]$|^[1][1-7][1-8][1-8]$/.test(value);
// Check if the value is a complete valid input
const isValid = /^[1][1-7][1-8][1-8]$/.test(value) && Number(value) <= 1788;
if (isPartialValid || isValid) {
if (text) text.textContent = value;
this.#previousValue = value;
} else {
this.#errorFunction();
input.value = this.#previousValue;
if (text) text.textContent = this.#previousValue;
}
}
}
#errorFunction() {
const element = this.getElement();
if (element) {
const input = element.querySelector(".input") as HTMLInputElement;
if (input) {
input.classList.add("error-flash");
setTimeout(() => {
input.classList.remove("error-flash");
}, 300); // Duration of the flash effect
}
}
}
#acceptInput(ev) {
if (ev.key === "Enter") this.#toggleEditMode(ev);
}
}

View File

@@ -0,0 +1,23 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
export class SpotMarker extends CustomMarker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.options.interactive = false;
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(
new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 26],
className: "leaflet-spot-marker",
})
);
var el = document.createElement("div");
el.classList.add("ol-spot-icon");
this.getElement()?.appendChild(el);
}
}

View File

@@ -0,0 +1,90 @@
.leaflet-spot-input-marker {
text-align: center;
display: flex !important;
justify-content: center;
align-items: center;
}
.leaflet-spot-input-marker .delete {
background-color: darkred;
color: #fffd;
border-radius: 999px;
font-size: 13px;
cursor: pointer;
height: 30px;
width: 30px;
font-weight: bold;
align-content: center;
}
.leaflet-spot-input-marker .delete:hover {
background-color: lightcoral;
}
.leaflet-spot-input-marker .container {
width: fit-content;
display: flex;
transform-origin: center;
background-color: var(--background-steel);
color: white;
border-radius: 999px;
font-size: 13px;
column-gap: 6px;
align-content: center;
}
.leaflet-spot-input-marker .text {
margin-left: 12px;
margin-top: auto;
margin-bottom: auto;
}
.leaflet-spot-input-marker .input {
display: none;
color: white;
appearance: none; /* Remove default appearance */
background-color: #333; /* Dark background */
border-radius: 8px; /* Rounded borders */
border: 1px solid #555; /* Border color */
padding: 4px 8px; /* Padding for better appearance */
outline: none; /* Remove default outline */
font-size: 13px;
width: 80px;
}
.leaflet-spot-input-marker .input:focus {
border-color: #777; /* Border color on focus */
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Shadow on focus */
}
.leaflet-spot-input-marker .container .input {
display: none;
}
.leaflet-spot-input-marker .container .text.edit-mode {
display: none;
}
.leaflet-spot-input-marker .input.error-flash {
animation: error-flash 0.3s;
}
@keyframes error-flash {
0% {
border-color: #555;
}
50% {
border-color: red;
}
100% {
border-color: #555;
}
}
.leaflet-spot-input-marker .delete.edit-mode {
background-color: green;
}
.leaflet-spot-input-marker .delete.edit-mode:hover {
background-color: lightgreen;
}

View File

@@ -141,6 +141,12 @@
width: 100%;
}
.ol-spot-icon {
background-image: url("/images/markers/target.svg");
height: 100%;
width: 100%;
}
.ol-text-icon {
color: #111111;
text-align: center;