diff --git a/libraries/commons/measurements/project.json b/libraries/commons/measurements/project.json
index 6d9384ab8e..97318c0c33 100644
--- a/libraries/commons/measurements/project.json
+++ b/libraries/commons/measurements/project.json
@@ -5,6 +5,13 @@
"projectType": "library",
"tags": [],
"targets": {
+ "build": {
+ "executor": "@nx/vite:build",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/libraries/commons/measurements"
+ }
+ },
"lint": {
"executor": "@nx/eslint:lint"
}
diff --git a/libraries/commons/measurements/src/index.d.ts b/libraries/commons/measurements/src/index.d.ts
index 7fc8c40bfe..7e6daf3eba 100644
--- a/libraries/commons/measurements/src/index.d.ts
+++ b/libraries/commons/measurements/src/index.d.ts
@@ -1,142 +1,16 @@
-export type ActiveShape = null | number | string | any;
-export enum MEASUREMENT_MODE {
- DEFAULT = "default",
- MEASUREMENT = "measurement",
-}
-
-export type MeasurementMapStatus =
- | "INACTIVE" // not active
- | "WAITING" // moving around not dragging anything and waiting for other stuff
- | "DRAWING" // either lines or polygons but in the process
- | "EDITING" // dragging vertices around
- | "MOVING"; // dragging whole objects around
-
-export interface MapMeasurementsContextType {
- mode: MEASUREMENT_MODE;
- setMode: (mode: MEASUREMENT_MODE) => void;
- shapes: any[];
- setShapes: (shapes: any[]) => void;
- activeShape: ActiveShape;
- setActiveShape: (shape: ActiveShape) => void;
- visibleShapes: any[];
- setVisibleShapes: (shapes: any[]) => void;
- snappingLatlng?: any;
- setSnappingLatlng?: (coords: any) => void;
-
- showAll: boolean;
- deleteAll: boolean;
- drawingShape: boolean;
- lastActiveShapeBeforeDrawing: null | any;
- moveToShape: null | any;
- updateShape: boolean;
- mapMovingEnd: boolean;
- updateTitleStatus: boolean;
- setDrawingShape: (drawingShape: boolean) => void;
- setShowAll: (showAll: boolean) => void;
- setDeleteAll: (deleteAll: boolean) => void;
- setMoveToShape: (moveToShape: any) => void;
- setUpdateShape: (updateShape: boolean) => void;
- setMapMovingEnd: (mapMovingEnd: boolean) => void;
- setUpdateTitleStatus: (updateTitleStatus: boolean) => void;
- setLastActiveShapeBeforeDrawing: (lastActiveShapeBeforeDrawing: any) => void;
- addShape: (layer: any) => void;
- deleteShapeById: (shapeId: string) => void;
- deleteVisibleShapeById: (shapeId: string) => void;
- updateShapeById: (
- shapeId: string,
- newCoordinates?: any,
- newDistance?: number,
- newSquare?: number | null
- ) => void;
- setLastVisibleShapeActive: () => void;
- setDrawingWithLastActiveShape: () => void;
- setActiveShapeIfDrawCancelled: () => void;
- toggleMeasurementMode: () => void;
- updateAreaOfDrawing: (newArea: number) => void;
- updateTitle: (shapeId: string | number, customTitle: string) => void;
- setStartDrawing: (status: boolean) => void;
- startDrawing: boolean;
- currentDrawHandler: any;
- setCurrentDrawHandler: (handler: any) => void;
- completeCurrentShape: () => void;
- config: MeasurementConfig;
- status: MeasurementMapStatus;
- setStatus: (status: MeasurementMapStatus) => void;
-}
-
-export interface MeasurementShapeDrawing {
- shapeId: number | string;
- number: number;
- coordinates?: unknown;
- [key: string]: unknown;
-}
-
-export type UIModeType = string | "measurement" | "default";
-
-export interface MapMeasurementProps {
- mode?: UIModeType;
- polygonActiveIcon?: string;
- polygonIcon?: string;
-}
-
-export interface MeasurementShape {
- shapeId: number | string;
- distance?: number;
- area?: number;
- customTitle?: string;
- shapeType?: "line" | "polygon" | string;
- [key: string]: unknown;
-}
-
-export interface InfoBoxMeasurementProps {
- collapsedInfoBox?: boolean;
- pixelWidth?: number;
-}
-
-export interface MeasurementTitleProps {
- title: string;
- shapeId: number | string;
- order: number;
- updateTitleMeasurementById: (shapeId: number | string, title: string) => void;
- setUpdateMeasurementStatus: (status: boolean) => void;
- isCollapsed?: boolean;
- collapsedContent?: string;
- editable?: boolean;
-}
-
-export interface MeasurementControlProps {
- isActive?: boolean;
- onToggle?: () => void;
- position?: "topleft" | "topright" | "bottomleft" | "bottomright";
- order?: number;
- iconBaseUrl?: string;
- icons?: {
- active: string;
- inactive: string;
- };
- altText?: string;
- iconClassName?: string;
-
- // Universal features
- disabled?: boolean;
- useDisabledStyle?: boolean;
- tooltip?: string | React.ReactNode;
- tooltipPlacement?: "top" | "bottom" | "left" | "right";
- className?: string;
- showInfoBox?: boolean;
-}
-
-export interface MeasurementConfig {
- editableTitle: boolean;
- infoBoxHeaderColor: string;
- localStorageKey: string;
- snappingEnabled: boolean;
- snappingOnUpdate: boolean;
- snappingQueryRadius: number;
- snappingMinZoom: number;
- snappingRadiusVisible: boolean;
- debugOutputMapStatus: boolean;
- debugOutputMapStatusPosition: { x: number; y: number };
-}
-
-export type PartialMeasurementConfig = Partial
;
+/**
+ * Public Type Exports
+ * Re-exports types for external consumption
+ */
+
+// Context types
+export type {
+ ActiveShape,
+ MapMeasurementsContextType,
+ MeasurementConfig,
+ PartialMeasurementConfig,
+ MeasurementMapStatus,
+} from "./lib/context/MapMeasurementsContext.d";
+
+// Component types
+export type { MeasurementShape } from "./lib/types/MeasurementShape";
diff --git a/libraries/commons/measurements/src/index.ts b/libraries/commons/measurements/src/index.ts
index 0e0530c7a5..3af9e57e17 100644
--- a/libraries/commons/measurements/src/index.ts
+++ b/libraries/commons/measurements/src/index.ts
@@ -1,11 +1,11 @@
export { InfoBoxMeasurement } from "./lib/components/InfoBoxMeasurement";
export { MeasurementControl } from "./lib/components/MeasurementControl";
+export { MeasurementTitle } from "./lib/components/MeasurementTitle";
export {
MapMeasurementsProvider,
useMapMeasurementsContext,
-} from "./lib/components/MapMeasurementsProvider";
-export { Measurements, MapMeasurementsObjects } from "./lib/lib-measurements";
-export { MeasurementsSnapping } from "./lib/components/MeasurementsSnapping";
-export { MeasurementStatusDebug } from "./lib/components/MeasurementStatusDebug";
+ defaultConfig,
+} from "./lib/context";
export { useMapLibreMap } from "./lib/hooks/useMapLibreMap";
+export { useMeasurements } from "./lib/hooks/useMeasurements";
export * from "./index.d";
diff --git a/libraries/commons/measurements/src/lib/components/InfoBoxMeasurement.tsx b/libraries/commons/measurements/src/lib/components/InfoBoxMeasurement.tsx
index 293a014dcd..4154299ee3 100644
--- a/libraries/commons/measurements/src/lib/components/InfoBoxMeasurement.tsx
+++ b/libraries/commons/measurements/src/lib/components/InfoBoxMeasurement.tsx
@@ -8,8 +8,14 @@ import "../styles/infoBox.css";
import { Tooltip } from "antd";
import { TopicMapContext } from "react-cismap/contexts/TopicMapContextProvider";
import { ResponsiveInfoBox } from "@carma-appframeworks/portals";
-import { useMapMeasurementsContext } from "./MapMeasurementsProvider";
-import { InfoBoxMeasurementProps, MeasurementShape } from "../..";
+import { useMapMeasurementsContext } from "../context";
+import { MeasurementShape } from "../types/MeasurementShape";
+import { MeasurementShapeData } from "../types/MeasurementShapeData";
+import { DRAWING_SHAPE_ID } from "../utils/constants";
+
+type InfoBoxMeasurementProps = {
+ pixelWidth?: number;
+};
export function InfoBoxMeasurement({
pixelWidth = 350,
@@ -38,7 +44,7 @@ export function InfoBoxMeasurement({
const [currentMeasure, setCurrentMeasure] = useState(0);
const [oldDataLength, setOldDataLength] = useState(measurementsData.length);
const [stepAfterMoveToShape, setStepAfterMoveToShape] = useState<
- number | string | null
+ number | string | symbol | null
>(null);
const [stepAfterUpdating, setStepAfterUpdating] = useState(false);
const [stepAfterCreating, setStepAfterCreating] = useState(false);
@@ -64,7 +70,7 @@ export function InfoBoxMeasurement({
useEffect(() => {
if (drawingMode) {
// setLastMeasureActive();
- setActiveShape(5555);
+ setActiveShape(DRAWING_SHAPE_ID);
return;
}
}, [drawingMode]);
@@ -147,7 +153,7 @@ export function InfoBoxMeasurement({
};
const activeShapeHandler = (
- shapeId: number | string | null
+ shapeId: number | string | symbol | null
): number | null => {
let activeShapePosition: number | null = null;
visibleShapesData.forEach((s, idx) => {
@@ -158,7 +164,9 @@ export function InfoBoxMeasurement({
return activeShapePosition;
};
- const getPositionInAllArray = (shapeId: number | string): number | null => {
+ const getPositionInAllArray = (
+ shapeId: number | string | symbol
+ ): number | null => {
let activeShapePosition: number | null = null;
measurementsData.forEach((s, idx) => {
if (s.shapeId === shapeId) {
@@ -168,9 +176,9 @@ export function InfoBoxMeasurement({
return activeShapePosition;
};
- const getOrderOfShape = (shapeId: number | string): number => {
+ const getOrderOfShape = (shapeId: number | string | symbol): number => {
let position: number;
- if (shapeId === 5555) {
+ if (shapeId === DRAWING_SHAPE_ID) {
position =
measurementsData.length === 0 ? 1 : measurementsData.length + 1;
} else {
@@ -183,7 +191,6 @@ export function InfoBoxMeasurement({
const deleteShapeHandler = (e: React.MouseEvent) => {
e.stopPropagation();
- // the method activate delete process in MapMeasurementsObjects
setDeleteAll(true);
// cleanUpdateMeasurementStatus();
// setLastMeasureActive();
@@ -199,7 +206,7 @@ export function InfoBoxMeasurement({
const setLastMeasureActive = () => {
// Set activeShape (source of truth) to the last visible shape
- if (activeShape === 5555) {
+ if (activeShape === DRAWING_SHAPE_ID) {
return;
}
@@ -218,9 +225,13 @@ export function InfoBoxMeasurement({
};
const updateTitleMeasurementById = (
- shapeId: number | string,
+ shapeId: number | string | symbol,
customTitle: string
) => {
+ if (typeof shapeId === "symbol") {
+ console.warn("Cannot update title for symbol shapeId", shapeId);
+ return;
+ }
updateTitle(shapeId, customTitle);
};
@@ -243,7 +254,7 @@ export function InfoBoxMeasurement({
{
- const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
- const isMobileUA =
- /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
- userAgent
- );
- const isSmallScreen =
- typeof window !== "undefined" &&
- window.matchMedia("(max-width: 768px)").matches;
- return isMobileUA || isSmallScreen;
-};
-
-const defaultConfig: MeasurementConfig = {
- editableTitle: true,
- infoBoxHeaderColor: "#3b82f6",
- localStorageKey: "measurementShapes",
- snappingEnabled: !isMobileDevice(), // Disable snapping on mobile
- snappingOnUpdate: false,
- snappingQueryRadius: 40,
- snappingMinZoom: 17,
- snappingRadiusVisible: false,
- debugOutputMapStatus: false,
- debugOutputMapStatusPosition: { x: 65, y: 15 },
-};
-
-export const MapMeasurementsContext = createContext(
- {
- mode: MEASUREMENT_MODE.DEFAULT,
- setMode: (mode: MEASUREMENT_MODE) => {},
- shapes: [],
- setShapes: (shapes: any[]) => {},
- activeShape: null,
- setActiveShape: (shape: ActiveShape) => {},
- visibleShapes: [],
- setVisibleShapes: (shapes: any[]) => {},
- snappingLatlng: null,
- setSnappingLatlng: (_latlng: any) => {},
- showAll: false,
- deleteAll: false,
- drawingShape: false,
- lastActiveShapeBeforeDrawing: null,
- moveToShape: null,
- updateShape: false,
- mapMovingEnd: false,
- updateTitleStatus: false,
- setDrawingShape: (drawingShape: boolean) => {},
- setShowAll: (showAll: boolean) => {},
- setDeleteAll: (deleteAll: boolean) => {},
- setMoveToShape: (moveToShape: any) => {},
- setUpdateShape: (updateShape: boolean) => {},
- setMapMovingEnd: (mapMovingEnd: boolean) => {},
- setUpdateTitleStatus: (updateTitleStatus: boolean) => {},
- setLastActiveShapeBeforeDrawing: (lastActiveShapeBeforeDrawing: any) => {},
- addShape: (layer: any) => {},
- deleteShapeById: (shapeId: string) => {},
- deleteVisibleShapeById: (shapeId: string) => {},
- updateShapeById: (
- shapeId: string,
- newCoordinates?: any,
- newDistance?: number,
- newSquare?: number | null
- ) => {},
- setLastVisibleShapeActive: () => {},
- setDrawingWithLastActiveShape: () => {},
- setActiveShapeIfDrawCancelled: () => {},
- toggleMeasurementMode: () => {},
- updateAreaOfDrawing: (newArea: number) => {},
- updateTitle: (_shapeId: string | number, _customTitle: string) => {},
- setStartDrawing: (status: boolean) => {},
- startDrawing: false,
- currentDrawHandler: null,
- setCurrentDrawHandler: (handler: any) => {},
- completeCurrentShape: () => {},
- config: defaultConfig,
- status: "INACTIVE" as MeasurementMapStatus,
- setStatus: (status: MeasurementMapStatus) => {},
- }
-);
-
-export const MapMeasurementsProvider = ({
- children,
- externalMode,
- setModeExternal,
- config = {},
-}: {
- children: React.ReactNode;
- externalMode?: MEASUREMENT_MODE;
- setModeExternal?: (mode: MEASUREMENT_MODE) => void;
- config?: PartialMeasurementConfig;
-}) => {
- // Internal mode state as fallback when external props not provided
- const [internalMode, setInternalMode] = useState(
- MEASUREMENT_MODE.DEFAULT
- );
-
- // Use external mode/setMode if provided, otherwise use internal state
- const mode = externalMode ?? internalMode;
- const setMode = setModeExternal ?? setInternalMode;
-
- // Merge provided config with defaults
- // Force disable snapping on mobile devices regardless of config
- const mergedConfig: MeasurementConfig = {
- ...defaultConfig,
- ...config,
- snappingEnabled: isMobileDevice()
- ? false
- : config.snappingEnabled ?? defaultConfig.snappingEnabled,
- };
- const [activeShape, setActiveShape] = useState(null);
- const [shapes, setShapes] = useState([]);
- const [visibleShapes, setVisibleShapes] = useState([]);
- const [snappingLatlng, setSnappingLatlng] = useState(null);
- const [showAll, setShowAll] = useState(false);
- const [deleteAll, setDeleteAll] = useState(false);
- const [drawingShape, setDrawingShape] = useState(false);
- const [lastActiveShapeBeforeDrawing, setLastActiveShapeBeforeDrawing] =
- useState(null);
- const [moveToShape, setMoveToShape] = useState(null);
- const [updateShape, setUpdateShape] = useState(false);
- const [mapMovingEnd, setMapMovingEnd] = useState(false);
- const [updateTitleStatus, setUpdateTitleStatus] = useState(false);
- const [startDrawing, setStartDrawing] = useState(false);
- const [currentDrawHandler, setCurrentDrawHandler] = useState(null);
- const [status, setStatus] = useState("INACTIVE");
-
- // Update status when mode changes
- useEffect(() => {
- if (mode === MEASUREMENT_MODE.MEASUREMENT) {
- setStatus("WAITING");
- } else {
- setStatus("INACTIVE");
- }
- }, [mode]);
-
- // Update status when drawing starts/ends
- useEffect(() => {
- if (drawingShape) {
- setStatus("DRAWING");
- } else if (mode === MEASUREMENT_MODE.MEASUREMENT) {
- // Only set to WAITING if not already in EDITING or MOVING state
- setStatus((currentStatus) => {
- if (currentStatus === "EDITING" || currentStatus === "MOVING") {
- return currentStatus;
- }
- return "WAITING";
- });
- }
- }, [drawingShape, mode]);
-
- useEffect(() => {
- setFromLocalforage(mergedConfig.localStorageKey, setShapes, []);
- }, []);
-
- useEffect(() => {
- saveToLocalforage(mergedConfig.localStorageKey, shapes);
- }, [shapes]);
-
- // useEffect(() => {
- // console.log("xxx visibleShapes", visibleShapes);
- // }, [visibleShapes]);
-
- // useEffect(() => {
- // console.log("xxx activeShape", activeShape);
- // }, [activeShape]);
-
- const addShape = (layer: any) => {
- setShapes((prevShapes) => [...prevShapes, layer]);
- };
-
- const deleteShapeById = (shapeId: string) => {
- setShapes((currentShapes) =>
- currentShapes.filter((shape) => shape.shapeId !== shapeId)
- );
- };
-
- const deleteVisibleShapeById = (shapeId: string) => {
- setVisibleShapes((currentVisibleShapes) =>
- currentVisibleShapes.filter((shape) => shape.shapeId !== shapeId)
- );
- };
-
- const updateShapeById = (
- shapeId: string,
- newCoordinates?: any,
- newDistance?: number,
- newSquare?: number | null
- ) => {
- setUpdateShape(true);
- setShapes((prevShapes) => {
- return prevShapes.map((s) => {
- if (s.shapeId === shapeId) {
- return {
- ...s,
- coordinates: newCoordinates,
- distance: newDistance,
- area: newSquare,
- };
- } else {
- return s;
- }
- });
- });
- };
-
- const setLastVisibleShapeActive = () => {
- setShapes((currentShapes) => {
- const lastShapeId = currentShapes[currentShapes.length - 1]?.shapeId;
- if (lastShapeId) {
- setActiveShape(lastShapeId);
- }
- return currentShapes;
- });
- };
-
- const setDrawingWithLastActiveShape = () => {
- setActiveShape((currentActiveShape) => {
- if (currentActiveShape) {
- setLastActiveShapeBeforeDrawing(currentActiveShape);
- setDrawingShape(true);
- }
- return currentActiveShape;
- });
- };
-
- const setActiveShapeIfDrawCancelled = () => {
- setLastActiveShapeBeforeDrawing((lastActiveShape) => {
- setVisibleShapes((visible) => {
- if (lastActiveShape && visible[0]?.shapeId !== 55555) {
- setActiveShape(lastActiveShape);
- setDrawingShape(false);
- } else {
- return []; // Clear visible shapes
- }
- return visible;
- });
- return lastActiveShape;
- });
- };
-
- const toggleMeasurementMode = () => {
- if (mode === MEASUREMENT_MODE.DEFAULT) {
- setMode(MEASUREMENT_MODE.MEASUREMENT);
- } else {
- setMode(MEASUREMENT_MODE.DEFAULT);
- setDrawingShape(false);
- setLastVisibleShapeActive();
- }
- };
-
- const updateAreaOfDrawing = (newArea: number) => {
- setVisibleShapes((visibleShapes) => {
- const shape = visibleShapes.map((s) => {
- if (s.shapeId === 5555) {
- return {
- ...s,
- area: newArea,
- };
- }
- return s;
- });
- return shape;
- });
- };
-
- const updateTitle = (shapeId: string | number, customTitle: string) => {
- setVisibleShapes((currentVisibleShapes) => {
- const shapeFromVisible = currentVisibleShapes.find(
- (s) => s.shapeId === shapeId
- );
-
- if (!shapeFromVisible) return currentVisibleShapes;
- return currentVisibleShapes.map((shape) => {
- if (shape.shapeId === shapeId) {
- return {
- ...shapeFromVisible,
- customTitle,
- };
- }
- return shape;
- });
- });
-
- // Update all shapes - find the shape first to preserve all properties
- setShapes((currentShapes) => {
- const shapeFromAllShapes = currentShapes.find(
- (s) => s.shapeId === shapeId
- );
- if (!shapeFromAllShapes) return currentShapes;
-
- return currentShapes.map((shape) => {
- if (shape.shapeId === shapeId) {
- return {
- ...shapeFromAllShapes,
- customTitle,
- };
- }
- return shape;
- });
- });
-
- // Set update title status to trigger any necessary UI updates
- setUpdateTitleStatus(true);
- };
-
- const completeCurrentShape = () => {
- if (
- currentDrawHandler &&
- typeof currentDrawHandler.completeShape === "function"
- ) {
- currentDrawHandler.completeShape();
- }
- };
-
- return (
-
- {children}
-
- );
-};
-
-export function useMapMeasurementsContext() {
- const ctx = useContext(MapMeasurementsContext);
- if (!ctx) {
- throw new Error(
- "useMapMeasurementsContext must be used within an MapMeasurementsProvider"
- );
- }
- return ctx;
-}
diff --git a/libraries/commons/measurements/src/lib/components/MeasurementControl.tsx b/libraries/commons/measurements/src/lib/components/MeasurementControl.tsx
index 7e7d8cc6ce..7900f4602b 100644
--- a/libraries/commons/measurements/src/lib/components/MeasurementControl.tsx
+++ b/libraries/commons/measurements/src/lib/components/MeasurementControl.tsx
@@ -5,11 +5,33 @@ import {
ControlButtonStyler,
} from "@carma-mapping/map-controls-layout";
import { InfoBoxMeasurement } from "./InfoBoxMeasurement";
-import { MeasurementControlProps, MEASUREMENT_MODE } from "../../index.d";
-import { useMapMeasurementsContext } from "./MapMeasurementsProvider";
+// import { MEASUREMENT_MODE } from "../context";
+import { useMapMeasurementsContext } from "../context";
import measureActive from "../assets/measure-active.png";
import measureInactive from "../assets/measure.png";
+interface MeasurementControlProps {
+ isActive?: boolean;
+ onToggle?: () => void;
+ position?: "topleft" | "topright" | "bottomleft" | "bottomright";
+ order?: number;
+ iconBaseUrl?: string;
+ icons?: {
+ active: string;
+ inactive: string;
+ };
+ altText?: string;
+ iconClassName?: string;
+
+ // Universal features
+ disabled?: boolean;
+ useDisabledStyle?: boolean;
+ tooltip?: string | React.ReactNode;
+ tooltipPlacement?: "top" | "bottom" | "left" | "right";
+ className?: string;
+ showInfoBox?: boolean;
+}
+
export const MeasurementControl = forwardRef<
HTMLButtonElement,
MeasurementControlProps
@@ -38,13 +60,12 @@ export const MeasurementControl = forwardRef<
},
ref
) => {
- const { mode, toggleMeasurementMode } = useMapMeasurementsContext();
+ const { isMeasurementEnabled, toggleMeasurementMode } =
+ useMapMeasurementsContext();
// Use context values if props are not provided
const isActive =
- propIsActive !== undefined
- ? propIsActive
- : mode === MEASUREMENT_MODE.MEASUREMENT;
+ propIsActive !== undefined ? propIsActive : isMeasurementEnabled;
const onToggle = propOnToggle || toggleMeasurementMode;
const getUrlPrefix = () => {
diff --git a/libraries/commons/measurements/src/lib/components/MeasurementStatusDebug.tsx b/libraries/commons/measurements/src/lib/components/MeasurementStatusDebug.tsx
deleted file mode 100644
index 597956b301..0000000000
--- a/libraries/commons/measurements/src/lib/components/MeasurementStatusDebug.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useMapMeasurementsContext } from "./MapMeasurementsProvider";
-
-export function MeasurementStatusDebug() {
- const { status, config } = useMapMeasurementsContext();
-
- if (!config.debugOutputMapStatus) {
- return null;
- }
-
- const { x, y } = config.debugOutputMapStatusPosition;
-
- return (
-
- {status}
-
- );
-}
diff --git a/libraries/commons/measurements/src/lib/components/MeasurementTitle.tsx b/libraries/commons/measurements/src/lib/components/MeasurementTitle.tsx
index e8b822c2ec..227af743a2 100644
--- a/libraries/commons/measurements/src/lib/components/MeasurementTitle.tsx
+++ b/libraries/commons/measurements/src/lib/components/MeasurementTitle.tsx
@@ -1,5 +1,18 @@
import { useState, useEffect } from "react";
-import { MeasurementTitleProps } from "../..";
+
+type MeasurementTitleProps = {
+ title: string;
+ shapeId: number | string | symbol;
+ order: number;
+ updateTitleMeasurementById: (
+ shapeId: number | string | symbol,
+ title: string
+ ) => void;
+ setUpdateMeasurementStatus: (status: boolean) => void;
+ isCollapsed?: boolean;
+ collapsedContent?: string;
+ editable?: boolean;
+};
const MeasurementTitle = ({
title,
@@ -51,6 +64,7 @@ const MeasurementTitle = ({
);
};
+export { MeasurementTitle };
export default MeasurementTitle;
function capitalizeFirstLetter(text: string): string {
diff --git a/libraries/commons/measurements/src/lib/components/MeasurementsSnapping.tsx b/libraries/commons/measurements/src/lib/components/MeasurementsSnapping.tsx
deleted file mode 100644
index 62c660cee8..0000000000
--- a/libraries/commons/measurements/src/lib/components/MeasurementsSnapping.tsx
+++ /dev/null
@@ -1,838 +0,0 @@
-import { useEffect, useRef, useContext, useState } from "react";
-import { TopicMapContext } from "react-cismap/contexts/TopicMapContextProvider";
-import { adjustClickPosition, toLatLngFromClosestPoint } from "../utils/helper";
-import { useMapMeasurementsContext } from "../components/MapMeasurementsProvider";
-import { SnappingPoint } from "../snapping/types";
-import {
- extractPointsFromGeometry,
- extractPointsFromMeasurementShape,
-} from "../snapping/utils/coordinateExtraction";
-
-export function MeasurementsSnapping({
- maplibreMaps,
-}: {
- maplibreMaps: any[];
-}) {
- const { routedMapRef } = useContext(TopicMapContext);
- const { shapes, setSnappingLatlng, config, currentDrawHandler, status } =
- useMapMeasurementsContext();
- const [queryRadius, setQueryRadius] = useState(config.snappingQueryRadius);
- const queryRadiusRef = useRef(queryRadius);
- const circleMarkerRef = useRef(null);
- const snappingIndicatorRef = useRef(null); // Leaflet marker for snapping point
- const shapesRef = useRef(shapes);
-
- // Use config directly
- const snappingEnabled = config.snappingEnabled;
- const snappingEnabledRef = useRef(snappingEnabled);
- const maplibreMapsRef = useRef(maplibreMaps);
- const lastHoveredMarkerRef = useRef(null);
- const isDraggingVertexRef = useRef(false);
- const currentDrawHandlerRef = useRef(currentDrawHandler);
- const lastSnappedCoordRef = useRef<[number, number] | null>(null);
- const statusRef = useRef(status);
-
- useEffect(() => {
- shapesRef.current = shapes;
- }, [shapes]);
-
- useEffect(() => {
- queryRadiusRef.current = queryRadius;
- }, [queryRadius]);
-
- useEffect(() => {
- statusRef.current = status;
- }, [status]);
-
- useEffect(() => {
- snappingEnabledRef.current = snappingEnabled;
- }, [snappingEnabled]);
-
- useEffect(() => {
- maplibreMapsRef.current = maplibreMaps;
- }, [maplibreMaps]);
-
- useEffect(() => {
- currentDrawHandlerRef.current = currentDrawHandler;
- }, [currentDrawHandler]);
-
- useEffect(() => {
- const leafletMap = routedMapRef?.leafletMap?.leafletElement;
-
- // Clean up visual indicators and coordinates when snapping is disabled
- if (!snappingEnabled) {
- // Clear snapping coordinates immediately
- if (setSnappingLatlng) {
- setSnappingLatlng(null);
- }
-
- // Remove all Leaflet markers
- if (leafletMap) {
- try {
- if (snappingIndicatorRef.current) {
- leafletMap.removeLayer(snappingIndicatorRef.current);
- snappingIndicatorRef.current = null;
- }
- if (circleMarkerRef.current) {
- leafletMap.removeLayer(circleMarkerRef.current);
- circleMarkerRef.current = null;
- }
- } catch (_) {
- // no-op safeguard
- }
- }
-
- // Clear cursor on all MapLibre maps
- maplibreMaps.forEach((map) => {
- if (map && map.getCanvas) {
- map.getCanvas().style.cursor = "";
- }
- });
-
- // Add handlers when snapping is disabled to prevent stale coordinates
- if (leafletMap && typeof leafletMap.on === "function") {
- const clearSnappingHandler = () => {
- if (setSnappingLatlng) {
- setSnappingLatlng(null);
- }
- };
-
- leafletMap.on("mousemove", clearSnappingHandler);
-
- // Add a mouseup handler that does NOT adjust click position
- const mapContainer = leafletMap.getContainer();
- const noAdjustHandler = (event: MouseEvent) => {
- // Do nothing - just let the event pass through normally
- // This prevents the old adjustClickPosition handler from being used
- };
-
- mapContainer.addEventListener("mouseup", noAdjustHandler, true);
-
- return () => {
- leafletMap.off("mousemove", clearSnappingHandler);
- mapContainer.removeEventListener("mouseup", noAdjustHandler, true);
- };
- }
-
- return;
- }
-
- if (leafletMap && typeof leafletMap.on === "function") {
- // Import L from leaflet
- const L = (window as any).L;
- let closestPoint: any = null;
- const closestPointRef = { current: null as any }; // Stable ref to preserve closestPoint
-
- // Centralized cleanup for markers and closestPoint
- const clearBlackPoint = () => {
- try {
- if (circleMarkerRef.current) {
- leafletMap.removeLayer(circleMarkerRef.current);
- circleMarkerRef.current = null;
- }
- if (snappingIndicatorRef.current) {
- leafletMap.removeLayer(snappingIndicatorRef.current);
- snappingIndicatorRef.current = null;
- }
- // Clear cursor on all MapLibre maps
- maplibreMaps.forEach((map) => {
- if (map && map.getCanvas) {
- map.getCanvas().style.cursor = "";
- }
- });
- closestPoint = null;
- closestPointRef.current = null;
- } catch (_) {
- // no-op safeguard
- }
- };
-
- const mousemoveHandler = (e: any) => {
- // Skip snapping indicator during vertex drag if snappingOnUpdate is disabled
- if (isDraggingVertexRef.current && !config.snappingOnUpdate) {
- clearBlackPoint();
- return;
- }
-
- // Check if ALT key is pressed - if so, disable snapping temporarily
- if (e.originalEvent.altKey) {
- clearBlackPoint();
- if (circleMarkerRef.current) {
- leafletMap.removeLayer(circleMarkerRef.current);
- circleMarkerRef.current = null;
- }
- if (setSnappingLatlng) {
- setSnappingLatlng(null);
- }
- return; // Exit early - no snapping while ALT is pressed
- }
-
- // Check zoom level - only work if zoom >= configured minimum
- const currentZoom = leafletMap.getZoom();
- if (currentZoom < config.snappingMinZoom) {
- // Zoom too low: centralized cleanup
- clearBlackPoint();
- return; // Exit early
- }
-
- // Remove old circle if exists
- if (circleMarkerRef.current) {
- leafletMap.removeLayer(circleMarkerRef.current);
- }
-
- const currentMaplibreMaps = maplibreMapsRef.current;
-
- // Get mouse position in lat/lng using Leaflet (always available)
- const mouseLatLng = leafletMap.mouseEventToLatLng(e.originalEvent);
- const mousePoint = leafletMap.latLngToContainerPoint(mouseLatLng);
-
- const currentRadius = queryRadiusRef.current;
-
- // Show radius circle if enabled and in WAITING or DRAWING status
- if (
- config.snappingRadiusVisible &&
- (statusRef.current === "WAITING" || statusRef.current === "DRAWING")
- ) {
- // Convert pixel radius to meters for the circle
- const metersPerPixel =
- (156543.03392 * Math.cos((mouseLatLng.lat * Math.PI) / 180)) /
- Math.pow(2, currentZoom);
- const radiusInMeters = currentRadius * metersPerPixel;
-
- circleMarkerRef.current = L.circle(mouseLatLng, {
- radius: radiusInMeters,
- color: "#ffffff",
- fillColor: "#ffffff",
- fillOpacity: 0.15,
- weight: 1,
- opacity: 0.4,
- interactive: false, // Don't capture mouse events
- }).addTo(leafletMap);
- }
- const coordinatePoints: SnappingPoint[] = [];
-
- // 1. Extract from vector features (loop through all MapLibre maps)
- currentMaplibreMaps.forEach((currentMaplibreMap) => {
- if (
- currentMaplibreMap &&
- currentMaplibreMap.getStyle &&
- currentMaplibreMap.getCanvas
- ) {
- try {
- const style = currentMaplibreMap.getStyle();
- if (style && style.layers) {
- const canvas = currentMaplibreMap.getCanvas();
- const rect = canvas.getBoundingClientRect();
- const point = {
- x: e.originalEvent.clientX - rect.left,
- y: e.originalEvent.clientY - rect.top,
- };
-
- const bbox = [
- [point.x - currentRadius, point.y - currentRadius],
- [point.x + currentRadius, point.y + currentRadius],
- ];
-
- const features = currentMaplibreMap.queryRenderedFeatures(
- bbox,
- {
- layers: style.layers
- .filter((layer: any) => {
- // Skip layers with skipSnapping metadata
- const skipSnapping =
- layer.metadata?.carmaConf?.skipSnapping;
- return (
- !skipSnapping && !layer.id.startsWith("highlight-")
- ); // if we have a layer with highlight- dont snap (thats only important if we are doing the snap vis dot in maplibre directly not atm. but we will keep it in here)
- })
- .map((layer: any) => layer.id),
- }
- );
-
- features.forEach((feature: any) => {
- const points = extractPointsFromGeometry(
- feature.geometry,
- "vector-features"
- );
- coordinatePoints.push(...points);
- });
- }
- } catch (error) {
- console.warn("Error extracting vector features:", error);
- }
- }
- });
-
- // 2. Extract from measurement shapes (independent of MapLibre)
- // Use shapesRef which is kept in sync via useEffect
- const currentShapes = shapesRef.current;
- currentShapes.forEach((shape: any) => {
- const points = extractPointsFromMeasurementShape(
- shape,
- "measurements"
- );
- coordinatePoints.push(...points);
- });
-
- // 3. Extract from in-progress drawing (if currently drawing)
- const currentDrawHandlerValue = currentDrawHandlerRef.current;
- if (
- currentDrawHandlerValue &&
- currentDrawHandlerValue._poly &&
- currentDrawHandlerValue._poly._latlngs
- ) {
- const drawingLatLngs = currentDrawHandlerValue._poly._latlngs;
- drawingLatLngs.forEach((latlng: any) => {
- coordinatePoints.push({
- coordinates: [latlng.lng, latlng.lat],
- sourceId: "drawing-in-progress",
- });
- });
- }
-
- // Filter points to only those within the query radius and calculate distances
- // Use Leaflet for coordinate projection (works without MapLibre)
- const filteredPointsWithDistance = coordinatePoints
- .map((snappingPoint: SnappingPoint) => {
- const coord = snappingPoint.coordinates;
- const pointLatLng = L.latLng(coord[1], coord[0]); // [lng, lat] -> L.latLng(lat, lng)
- const projectedPoint =
- leafletMap.latLngToContainerPoint(pointLatLng);
-
- const dx = projectedPoint.x - mousePoint.x;
- const dy = projectedPoint.y - mousePoint.y;
- const distance = Math.sqrt(dx * dx + dy * dy);
-
- return { snappingPoint, distance };
- })
- .filter((item) => item.distance <= currentRadius);
-
- // Find the shortest distance
- let shortestDistance = Infinity;
- let shortestIndex = -1;
-
- filteredPointsWithDistance.forEach((item: any, index: number) => {
- if (item.distance < shortestDistance) {
- shortestDistance = item.distance;
- shortestIndex = index;
- }
- });
-
- // Determine snapping point
- const blackPoint: any[] = [];
- let isSnapped = false;
-
- if (shortestIndex === -1) {
- // No points found - use mouse pointer but don't show indicator
- blackPoint.push({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: [mouseLatLng.lng, mouseLatLng.lat],
- },
- properties: { black: true },
- });
- isSnapped = false;
- } else {
- // Snap to the closest point found within query radius
- const closestItem = filteredPointsWithDistance[shortestIndex];
- blackPoint.push({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: closestItem.snappingPoint.coordinates,
- },
- properties: {
- black: true,
- source: closestItem.snappingPoint.sourceId, // Pass source for polygon closure check
- },
- });
- isSnapped = true;
- }
- closestPoint = blackPoint[0];
- closestPointRef.current = blackPoint[0];
-
- const finalLatLng = toLatLngFromClosestPoint(closestPoint);
- if (finalLatLng && setSnappingLatlng) {
- // Check if we're snapping to the first vertex of an in-progress drawing
- // If so, only allow snapping if we're within the query radius (prevents premature polygon closure)
- let shouldSnap = true;
- if (
- currentDrawHandlerValue &&
- currentDrawHandlerValue._markers &&
- currentDrawHandlerValue._poly?._latlngs &&
- currentDrawHandlerValue._poly._latlngs.length >= 3
- ) {
- const firstVertex = currentDrawHandlerValue._poly._latlngs[0];
- const threshold = 0.0001; // ~11 meters
-
- // Check if finalLatLng matches first vertex
- if (
- Math.abs(finalLatLng.lat - firstVertex.lat) < threshold &&
- Math.abs(finalLatLng.lng - firstVertex.lng) < threshold
- ) {
- // We're trying to snap to first vertex - check pixel distance from mouse
- const map = routedMapRef.current?.leafletMap?.leafletElement;
- if (map && e.latlng) {
- const mousePoint = map.latLngToContainerPoint(e.latlng);
- const vertexPoint = map.latLngToContainerPoint(firstVertex);
- const pixelDistance = Math.sqrt(
- Math.pow(mousePoint.x - vertexPoint.x, 2) +
- Math.pow(mousePoint.y - vertexPoint.y, 2)
- );
-
- // Only snap if mouse is within query radius
- if (pixelDistance > queryRadius) {
- shouldSnap = false;
- }
- }
- }
- }
-
- if (shouldSnap) {
- setSnappingLatlng(finalLatLng);
- } else {
- setSnappingLatlng(null);
- }
- }
-
- // Trigger vertex marker hover for tooltip/area preview (Phase 3)
- // Check if we snapped to the first vertex of in-progress drawing
- if (
- isSnapped &&
- currentDrawHandlerValue &&
- currentDrawHandlerValue._markers &&
- shortestIndex !== -1
- ) {
- const latlngs = currentDrawHandlerValue._poly?._latlngs;
- if (latlngs && latlngs.length >= 3) {
- const firstVertex = latlngs[0];
- const snappedItem = filteredPointsWithDistance[shortestIndex];
- const snappedCoord = snappedItem.snappingPoint.coordinates;
- const threshold = 0.0001; // ~11 meters
-
- // Check if snapped point matches first vertex coordinates (regardless of source)
- // This handles both drawing-in-progress points AND vector features at same location
- if (
- Math.abs(snappedCoord[1] - firstVertex.lat) < threshold &&
- Math.abs(snappedCoord[0] - firstVertex.lng) < threshold
- ) {
- // Fire mouseover on first vertex marker to show area preview
- const firstMarker = currentDrawHandlerValue._markers[0];
- if (firstMarker && lastHoveredMarkerRef.current !== firstMarker) {
- lastHoveredMarkerRef.current = firstMarker;
- firstMarker.fire("mouseover", { target: firstMarker });
- }
- } else {
- // Snapped to different point - fire mouseout
- if (lastHoveredMarkerRef.current) {
- lastHoveredMarkerRef.current.fire("mouseout", {
- target: lastHoveredMarkerRef.current,
- });
- lastHoveredMarkerRef.current = null;
- }
- }
- }
- } else {
- // Not snapped or no drawing - fire mouseout if we were hovering
- if (lastHoveredMarkerRef.current) {
- lastHoveredMarkerRef.current.fire("mouseout", {
- target: lastHoveredMarkerRef.current,
- });
- lastHoveredMarkerRef.current = null;
- }
- }
-
- // Only update indicator if snap position changed
- const currentCoord: [number, number] | null =
- finalLatLng && isSnapped ? [finalLatLng.lng, finalLatLng.lat] : null;
-
- const lastCoord = lastSnappedCoordRef.current;
- const coordChanged =
- !lastCoord ||
- !currentCoord ||
- Math.abs(lastCoord[0] - currentCoord[0]) > 0.00001 ||
- Math.abs(lastCoord[1] - currentCoord[1]) > 0.00001;
-
- if (coordChanged) {
- // Remove old snapping indicator if exists
- if (snappingIndicatorRef.current) {
- leafletMap.removeLayer(snappingIndicatorRef.current);
- snappingIndicatorRef.current = null;
- }
-
- // Create Leaflet marker for snapping indicator ONLY when snapped
- // Match the size of measurement handles (8px total = 4px radius)
- if (
- finalLatLng &&
- isSnapped &&
- (statusRef.current === "WAITING" || statusRef.current === "DRAWING")
- ) {
- snappingIndicatorRef.current = L.circleMarker(
- [finalLatLng.lat, finalLatLng.lng],
- {
- radius: 3.5,
- color: "#000000",
- fillColor: "#000000",
- fillOpacity: 0.8,
- weight: 1,
- opacity: 0.8,
- }
- ).addTo(leafletMap);
- }
-
- lastSnappedCoordRef.current = currentCoord;
- }
- };
-
- const mouseoutHandler = () => {
- // Remove circle and snapping indicator when mouse leaves map
- if (circleMarkerRef.current) {
- leafletMap.removeLayer(circleMarkerRef.current);
- circleMarkerRef.current = null;
- }
- if (snappingIndicatorRef.current) {
- leafletMap.removeLayer(snappingIndicatorRef.current);
- snappingIndicatorRef.current = null;
- }
- };
-
- leafletMap.on("mousemove", mousemoveHandler);
- leafletMap.on("mouseout", mouseoutHandler);
-
- // Phase 4: Show snap indicator during vertex drag
- const vertexDragHandler = (e: any) => {
- isDraggingVertexRef.current = true;
-
- if (!snappingEnabledRef.current || !config.snappingOnUpdate) return;
-
- const vertex = e.vertex;
- if (!vertex) return;
-
- // Get current vertex position during drag
- const vertexLatLng = vertex.latlng;
- const vertexPoint = leafletMap.latLngToContainerPoint(vertexLatLng);
-
- const currentRadius = queryRadiusRef.current;
- const coordinatePoints: SnappingPoint[] = [];
-
- // Extract snap points from vector features
- const currentMaplibreMaps = maplibreMapsRef.current;
- currentMaplibreMaps.forEach((currentMaplibreMap) => {
- if (
- currentMaplibreMap &&
- currentMaplibreMap.getStyle &&
- currentMaplibreMap.getCanvas
- ) {
- try {
- const style = currentMaplibreMap.getStyle();
- if (style && style.layers) {
- const canvas = currentMaplibreMap.getCanvas();
- const rect = canvas.getBoundingClientRect();
-
- // Convert Leaflet container point to MapLibre canvas point
- const mapContainer = leafletMap.getContainer();
- const mapRect = mapContainer.getBoundingClientRect();
- const canvasX = vertexPoint.x + mapRect.left - rect.left;
- const canvasY = vertexPoint.y + mapRect.top - rect.top;
-
- const bbox = [
- [canvasX - currentRadius, canvasY - currentRadius],
- [canvasX + currentRadius, canvasY + currentRadius],
- ];
-
- const features = currentMaplibreMap.queryRenderedFeatures(
- bbox,
- {
- layers: style.layers
- .filter((layer: any) => {
- // Skip layers with skipSnapping metadata
- const skipSnapping =
- layer.metadata?.carmaConf?.skipSnapping;
- return (
- !skipSnapping && !layer.id.startsWith("highlight-")
- );
- })
- .map((layer: any) => layer.id),
- }
- );
-
- features.forEach((feature: any) => {
- const points = extractPointsFromGeometry(
- feature.geometry,
- "vector-features"
- );
- coordinatePoints.push(...points);
- });
- }
- } catch (error) {
- console.warn("Error extracting vector features:", error);
- }
- }
- });
-
- // Extract from other measurement shapes
- const currentShapes = shapesRef.current;
- currentShapes.forEach((shape: any) => {
- const points = extractPointsFromMeasurementShape(
- shape,
- "measurements"
- );
- coordinatePoints.push(...points);
- });
-
- // Filter out the vertex being dragged (exclude self-snapping)
- const threshold = 0.00001; // Very small threshold to identify same point
- const filteredCoordinatePoints = coordinatePoints.filter((point) => {
- const pointLatLng = L.latLng(
- point.coordinates[1],
- point.coordinates[0]
- );
- return !(
- Math.abs(pointLatLng.lat - vertexLatLng.lat) < threshold &&
- Math.abs(pointLatLng.lng - vertexLatLng.lng) < threshold
- );
- });
-
- // Find closest point within radius
- const filteredPointsWithDistance = filteredCoordinatePoints
- .map((snappingPoint: SnappingPoint) => {
- const coord = snappingPoint.coordinates;
- const pointLatLng = L.latLng(coord[1], coord[0]);
- const projectedPoint =
- leafletMap.latLngToContainerPoint(pointLatLng);
-
- const dx = projectedPoint.x - vertexPoint.x;
- const dy = projectedPoint.y - vertexPoint.y;
- const distance = Math.sqrt(dx * dx + dy * dy);
-
- return { snappingPoint, distance };
- })
- .filter((item) => item.distance <= currentRadius);
-
- // Remove old indicator
- if (snappingIndicatorRef.current) {
- leafletMap.removeLayer(snappingIndicatorRef.current);
- snappingIndicatorRef.current = null;
- }
-
- if (filteredPointsWithDistance.length > 0) {
- // Find shortest distance
- let shortestDistance = Infinity;
- let shortestIndex = -1;
- filteredPointsWithDistance.forEach((item, index) => {
- if (item.distance < shortestDistance) {
- shortestDistance = item.distance;
- shortestIndex = index;
- }
- });
-
- if (shortestIndex !== -1) {
- const closestItem = filteredPointsWithDistance[shortestIndex];
- const snappedCoord = closestItem.snappingPoint.coordinates;
- // Show snap indicator at target location
- snappingIndicatorRef.current = L.circleMarker(
- [snappedCoord[1], snappedCoord[0]],
- {
- radius: 3.5,
- color: "#000000",
- fillColor: "#000000",
- fillOpacity: 0.8,
- weight: 1,
- opacity: 0.8,
- }
- ).addTo(leafletMap);
- }
- }
- };
-
- leafletMap.on("editable:vertex:drag", vertexDragHandler);
-
- // Phase 4: Snap vertex AFTER drag ends
- const vertexDragEndHandler = (e: any) => {
- isDraggingVertexRef.current = false;
-
- if (!snappingEnabledRef.current || !config.snappingOnUpdate) return;
-
- const vertex = e.vertex;
- if (!vertex) return;
-
- // Get final vertex position after drag
- const vertexLatLng = vertex.latlng;
- const vertexPoint = leafletMap.latLngToContainerPoint(vertexLatLng);
-
- const currentRadius = queryRadiusRef.current;
- const coordinatePoints: SnappingPoint[] = [];
-
- // Extract snap points from vector features
- const currentMaplibreMaps = maplibreMapsRef.current;
- currentMaplibreMaps.forEach((currentMaplibreMap) => {
- if (
- currentMaplibreMap &&
- currentMaplibreMap.getStyle &&
- currentMaplibreMap.getCanvas
- ) {
- try {
- const style = currentMaplibreMap.getStyle();
- if (style && style.layers) {
- const canvas = currentMaplibreMap.getCanvas();
- const rect = canvas.getBoundingClientRect();
-
- // Convert Leaflet container point to MapLibre canvas point
- const mapContainer = leafletMap.getContainer();
- const mapRect = mapContainer.getBoundingClientRect();
- const canvasX = vertexPoint.x + mapRect.left - rect.left;
- const canvasY = vertexPoint.y + mapRect.top - rect.top;
-
- const bbox = [
- [canvasX - currentRadius, canvasY - currentRadius],
- [canvasX + currentRadius, canvasY + currentRadius],
- ];
-
- const features = currentMaplibreMap.queryRenderedFeatures(
- bbox,
- {
- layers: style.layers
- .filter((layer: any) => {
- // Skip layers with skipSnapping metadata
- const skipSnapping =
- layer.metadata?.carmaConf?.skipSnapping;
- return (
- !skipSnapping && !layer.id.startsWith("highlight-")
- );
- })
- .map((layer: any) => layer.id),
- }
- );
-
- features.forEach((feature: any) => {
- const points = extractPointsFromGeometry(
- feature.geometry,
- "vector-features"
- );
- coordinatePoints.push(...points);
- });
- }
- } catch (error) {
- console.warn("Error extracting vector features:", error);
- }
- }
- });
-
- // Extract from other measurement shapes
- const currentShapes = shapesRef.current;
- currentShapes.forEach((shape: any) => {
- const points = extractPointsFromMeasurementShape(
- shape,
- "measurements"
- );
- coordinatePoints.push(...points);
- });
-
- // Filter out the vertex being dragged (exclude self-snapping)
- const threshold = 0.00001; // Very small threshold to identify same point
- const filteredCoordinatePoints = coordinatePoints.filter((point) => {
- const pointLatLng = L.latLng(
- point.coordinates[1],
- point.coordinates[0]
- );
- return !(
- Math.abs(pointLatLng.lat - vertexLatLng.lat) < threshold &&
- Math.abs(pointLatLng.lng - vertexLatLng.lng) < threshold
- );
- });
-
- // Find closest point within radius
- const filteredPointsWithDistance = filteredCoordinatePoints
- .map((snappingPoint: SnappingPoint) => {
- const coord = snappingPoint.coordinates;
- const pointLatLng = L.latLng(coord[1], coord[0]);
- const projectedPoint =
- leafletMap.latLngToContainerPoint(pointLatLng);
-
- const dx = projectedPoint.x - vertexPoint.x;
- const dy = projectedPoint.y - vertexPoint.y;
- const distance = Math.sqrt(dx * dx + dy * dy);
-
- return { snappingPoint, distance };
- })
- .filter((item) => item.distance <= currentRadius);
-
- if (filteredPointsWithDistance.length > 0) {
- // Find shortest distance
- let shortestDistance = Infinity;
- let shortestIndex = -1;
- filteredPointsWithDistance.forEach((item, index) => {
- if (item.distance < shortestDistance) {
- shortestDistance = item.distance;
- shortestIndex = index;
- }
- });
-
- if (shortestIndex !== -1) {
- const closestItem = filteredPointsWithDistance[shortestIndex];
- const snappedCoord = closestItem.snappingPoint.coordinates;
- // Snap vertex to final position
- vertex.latlng.lat = snappedCoord[1];
- vertex.latlng.lng = snappedCoord[0];
- vertex.update();
- // Force complete refresh of the editor to recalculate middle markers
- if (e.layer.editor) {
- // Reset the editor to force recalculation
- e.layer.editor.reset();
- }
- e.layer.redraw();
- }
- }
- };
-
- leafletMap.on("editable:vertex:dragend", vertexDragEndHandler);
-
- // Add DOM listener in CAPTURE phase to intercept before Leaflet
- const mapContainer = leafletMap.getContainer();
- const mouseupHandler = (event: MouseEvent) => {
- // Only adjust if snapping is enabled
- if (snappingEnabledRef.current) {
- const snapPoint = closestPointRef.current;
- adjustClickPosition(
- event,
- snapPoint,
- "mouseup",
- leafletMap,
- currentDrawHandlerRef.current
- );
- }
- };
- mapContainer.addEventListener("mouseup", mouseupHandler, true);
- // mapContainer.addEventListener(
- // "click",
- // (event: MouseEvent) =>
- // adjustClickPosition(event, closestPoint, "click", leafletMap),
- // true
- // );
-
- // Cleanup function to remove listeners and markers
- return () => {
- leafletMap.off("mousemove", mousemoveHandler);
- leafletMap.off("mouseout", mouseoutHandler);
- leafletMap.off("editable:vertex:drag", vertexDragHandler);
- leafletMap.off("editable:vertex:dragend", vertexDragEndHandler);
- mapContainer.removeEventListener("mouseup", mouseupHandler, true);
- if (circleMarkerRef.current) {
- leafletMap.removeLayer(circleMarkerRef.current);
- circleMarkerRef.current = null;
- }
- if (snappingIndicatorRef.current) {
- leafletMap.removeLayer(snappingIndicatorRef.current);
- snappingIndicatorRef.current = null;
- }
- };
- }
- }, [
- routedMapRef,
- snappingEnabled,
- config.snappingMinZoom,
- setSnappingLatlng,
- ]);
- return null;
-}
diff --git a/libraries/commons/measurements/src/lib/context/MapMeasurementsContext.d.ts b/libraries/commons/measurements/src/lib/context/MapMeasurementsContext.d.ts
new file mode 100644
index 0000000000..0314a40ac1
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/context/MapMeasurementsContext.d.ts
@@ -0,0 +1,30 @@
+/**
+ * Map Measurements Context Types
+ * Shared types used by context, provider, and external consumers
+ */
+
+export type ActiveShape = null | number | string | any;
+
+export type MeasurementMapStatus =
+ | "INACTIVE" // not active
+ | "WAITING" // moving around not dragging anything and waiting for other stuff
+ | "DRAWING" // either lines or polygons but in the process
+ | "EDITING" // dragging vertices around
+ | "MOVING"; // dragging whole objects around
+
+export interface MeasurementConfig {
+ editableTitle: boolean;
+ infoBoxHeaderColor: string;
+ localStorageKey: string;
+ snappingEnabled: boolean;
+ snappingOnUpdate: boolean;
+ snappingQueryRadius: number;
+ snappingMinZoom: number;
+ snappingRadiusVisible: boolean;
+ /** Minimum distance in meters to consider two points identical (default 0.1m = 10cm) */
+ snappingIdentityDistanceMeters: number;
+ debugOutputMapStatus: boolean;
+ debugOutputMapStatusPosition: { x: number; y: number };
+}
+
+export type PartialMeasurementConfig = Partial;
diff --git a/libraries/commons/measurements/src/lib/context/MapMeasurementsContext.tsx b/libraries/commons/measurements/src/lib/context/MapMeasurementsContext.tsx
new file mode 100644
index 0000000000..f5ab6dc006
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/context/MapMeasurementsContext.tsx
@@ -0,0 +1,128 @@
+import { createContext } from "react";
+import type {
+ ActiveShape,
+ MeasurementConfig,
+ MeasurementMapStatus,
+} from "./MapMeasurementsContext.d";
+// import { MEASUREMENT_MODE } from "./MapMeasurementsContext.d";
+
+export interface MapMeasurementsContextType {
+ isMeasurementEnabled: boolean;
+ setMeasurementEnabled: (enabled: boolean) => void;
+ shapes: any[];
+ setShapes: (shapes: any[]) => void;
+ activeShape: ActiveShape;
+ setActiveShape: (shape: ActiveShape) => void;
+ visibleShapes: any[];
+ setVisibleShapes: (shapes: any[]) => void;
+
+ showAll: boolean;
+ deleteAll: boolean;
+ drawingShape: boolean;
+ lastActiveShapeBeforeDrawing: null | any;
+ moveToShape: null | any;
+ updateShape: boolean;
+ mapMovingEnd: boolean;
+ updateTitleStatus: boolean;
+ setDrawingShape: (drawingShape: boolean) => void;
+ setShowAll: (showAll: boolean) => void;
+ setDeleteAll: (deleteAll: boolean) => void;
+ setMoveToShape: (moveToShape: any) => void;
+ setUpdateShape: (updateShape: boolean) => void;
+ setMapMovingEnd: (mapMovingEnd: boolean) => void;
+ setUpdateTitleStatus: (updateTitleStatus: boolean) => void;
+ setLastActiveShapeBeforeDrawing: (lastActiveShapeBeforeDrawing: any) => void;
+ addShape: (layer: any) => void;
+ deleteShapeById: (shapeId: string) => void;
+ deleteVisibleShapeById: (shapeId: string) => void;
+ updateShapeById: (
+ shapeId: string,
+ newCoordinates?: any,
+ newDistance?: number,
+ newSquare?: number | null
+ ) => void;
+ setLastVisibleShapeActive: () => void;
+ setDrawingWithLastActiveShape: () => void;
+ setActiveShapeIfDrawCancelled: () => void;
+ toggleMeasurementMode: () => void;
+ // NOTE: newArea is pre-formatted string like "123.45 m²" or "1.23 km²" from calculateArea()
+ updateAreaOfDrawing: (newArea: string) => void;
+ updateTitle: (shapeId: string | number, customTitle: string) => void;
+ config: MeasurementConfig;
+ isSnapping: boolean;
+ setIsSnapping: (isSnapping: boolean) => void;
+}
+
+// Detect mobile devices
+const isMobileDevice = () => {
+ const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
+ const isMobileUA =
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
+ userAgent
+ );
+ const isSmallScreen =
+ typeof window !== "undefined" &&
+ window.matchMedia("(max-width: 768px)").matches;
+ return isMobileUA || isSmallScreen;
+};
+
+const defaultConfig: MeasurementConfig = {
+ editableTitle: true,
+ infoBoxHeaderColor: "#3b82f6",
+ localStorageKey: "measurementShapes",
+ snappingEnabled: !isMobileDevice(), // Disable snapping on mobile
+ snappingOnUpdate: false,
+ snappingQueryRadius: 40,
+ snappingMinZoom: 17,
+ snappingRadiusVisible: false,
+ snappingIdentityDistanceMeters: 0.1, // 10cm - points closer than this are considered identical
+ debugOutputMapStatus: false,
+ debugOutputMapStatusPosition: { x: 65, y: 15 },
+};
+
+export const MapMeasurementsContext = createContext(
+ {
+ isMeasurementEnabled: false,
+ setMeasurementEnabled: (enabled: boolean) => {},
+ shapes: [],
+ setShapes: (shapes: any[]) => {},
+ activeShape: null,
+ setActiveShape: (shape: ActiveShape) => {},
+ visibleShapes: [],
+ setVisibleShapes: (shapes: any[]) => {},
+ showAll: false,
+ deleteAll: false,
+ drawingShape: false,
+ lastActiveShapeBeforeDrawing: null,
+ moveToShape: null,
+ updateShape: false,
+ mapMovingEnd: false,
+ updateTitleStatus: false,
+ setDrawingShape: (drawingShape: boolean) => {},
+ setShowAll: (showAll: boolean) => {},
+ setDeleteAll: (deleteAll: boolean) => {},
+ setMoveToShape: (moveToShape: any) => {},
+ setUpdateShape: (updateShape: boolean) => {},
+ setMapMovingEnd: (mapMovingEnd: boolean) => {},
+ setUpdateTitleStatus: (updateTitleStatus: boolean) => {},
+ setLastActiveShapeBeforeDrawing: (lastActiveShapeBeforeDrawing: any) => {},
+ addShape: (layer: any) => {},
+ deleteShapeById: (shapeId: string) => {},
+ deleteVisibleShapeById: (shapeId: string) => {},
+ updateShapeById: (
+ shapeId: string,
+ newCoordinates?: any,
+ newDistance?: number,
+ newSquare?: number | null
+ ) => {},
+ setLastVisibleShapeActive: () => {},
+ setDrawingWithLastActiveShape: () => {},
+ setActiveShapeIfDrawCancelled: () => {},
+ toggleMeasurementMode: () => {},
+ updateAreaOfDrawing: (newArea: string) => {},
+ updateTitle: (shapeId: string | number, customTitle: string) => {},
+ config: defaultConfig,
+ isSnapping: true,
+ setIsSnapping: (isSnapping: boolean) => {},
+ }
+);
diff --git a/libraries/commons/measurements/src/lib/context/MapMeasurementsProvider.tsx b/libraries/commons/measurements/src/lib/context/MapMeasurementsProvider.tsx
new file mode 100644
index 0000000000..1386829a70
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/context/MapMeasurementsProvider.tsx
@@ -0,0 +1,341 @@
+import { useEffect, useState, useRef, useCallback, useMemo } from "react";
+import type {
+ ActiveShape,
+ MeasurementConfig,
+ MeasurementMapStatus,
+ PartialMeasurementConfig,
+} from "./MapMeasurementsContext.d";
+// import { MEASUREMENT_MODE } from "./MapMeasurementsContext.d";
+import { MapMeasurementsContext } from "./MapMeasurementsContext";
+import { setFromLocalforage, saveToLocalforage } from "../utils/storage";
+import { normalizeOptions } from "@carma-commons/utils";
+import { DRAWING_SHAPE_ID } from "../utils/constants";
+
+// Detect mobile devices
+const isMobileDevice = () => {
+ const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
+ const isMobileUA =
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
+ userAgent
+ );
+ const isSmallScreen =
+ typeof window !== "undefined" &&
+ window.matchMedia("(max-width: 768px)").matches;
+ return isMobileUA || isSmallScreen;
+};
+
+export const defaultConfig: MeasurementConfig = {
+ editableTitle: true,
+ infoBoxHeaderColor: "#3b82f6",
+ localStorageKey: "measurementShapes",
+ snappingEnabled: !isMobileDevice(), // Disable snapping on mobile
+ snappingOnUpdate: false,
+ snappingQueryRadius: 40,
+ snappingMinZoom: 17,
+ snappingRadiusVisible: false,
+ snappingIdentityDistanceMeters: 0.1, // 10cm - points closer than this are considered identical
+ debugOutputMapStatus: false,
+ debugOutputMapStatusPosition: { x: 65, y: 15 },
+};
+
+export const MapMeasurementsProvider = ({
+ children,
+ config = {},
+ snappingEnabled,
+}: {
+ children: React.ReactNode;
+ config?: PartialMeasurementConfig;
+ snappingEnabled?: boolean;
+}) => {
+ const [isMeasurementEnabled, setMeasurementEnabled] =
+ useState(false);
+
+ const mergedConfig: MeasurementConfig = useMemo(() => {
+ const opts = normalizeOptions(config, defaultConfig);
+ if (snappingEnabled !== undefined) {
+ opts.snappingEnabled = snappingEnabled;
+ }
+ if (isMobileDevice()) {
+ opts.snappingEnabled = false;
+ }
+ return opts;
+ }, [config, snappingEnabled]);
+
+ const [isSnapping, setIsSnapping] = useState(
+ mergedConfig.snappingEnabled
+ );
+
+ useEffect(() => {
+ setIsSnapping(mergedConfig.snappingEnabled);
+ }, [mergedConfig.snappingEnabled]);
+
+ useEffect(() => {
+ console.debug("[MapMeasurementsProvider] Config initialized:", {
+ isMobile: isMobileDevice(),
+ snappingEnabled: mergedConfig.snappingEnabled,
+ providedConfig: config,
+ });
+ }, []);
+ const [activeShape, setActiveShape] = useState(null);
+ const [shapes, setShapes] = useState([]);
+ const [visibleShapes, setVisibleShapes] = useState([]);
+
+ const [showAll, setShowAll] = useState(false);
+ const [deleteAll, setDeleteAll] = useState(false);
+ const [drawingShape, setDrawingShape] = useState(false);
+
+ const [lastActiveShapeBeforeDrawing, setLastActiveShapeBeforeDrawing] =
+ useState(null);
+ const [moveToShape, setMoveToShape] = useState(null);
+ const [updateShape, setUpdateShape] = useState(false);
+ const [mapMovingEnd, setMapMovingEnd] = useState(false);
+ const [updateTitleStatus, setUpdateTitleStatus] = useState(false);
+
+ // DEBUG: Log Provider mount/unmount
+ useEffect(() => {
+ console.warn("[MapMeasurementsProvider] MOUNTED");
+ return () => {
+ console.error(
+ "[MapMeasurementsProvider] UNMOUNTED - THIS SHOULD NOT HAPPEN DURING MEASUREMENT!"
+ );
+ };
+ }, []);
+
+ useEffect(() => {
+ setFromLocalforage(mergedConfig.localStorageKey, setShapes, []);
+ }, []);
+
+ useEffect(() => {
+ saveToLocalforage(mergedConfig.localStorageKey, shapes);
+ }, [shapes]);
+
+ // useEffect(() => {
+ // console.log("xxx visibleShapes", visibleShapes);
+ // }, [visibleShapes]);
+
+ // useEffect(() => {
+ // console.log("xxx activeShape", activeShape);
+ // }, [activeShape]);
+
+ const addShape = useCallback((layer: any) => {
+ setShapes((prevShapes) => [...prevShapes, layer]);
+ }, []);
+
+ const deleteShapeById = useCallback((shapeId: string) => {
+ setShapes((currentShapes) =>
+ currentShapes.filter((shape) => shape.shapeId !== shapeId)
+ );
+ }, []);
+
+ const deleteVisibleShapeById = useCallback((shapeId: string) => {
+ setVisibleShapes((currentVisibleShapes) =>
+ currentVisibleShapes.filter((shape) => shape.shapeId !== shapeId)
+ );
+ }, []);
+
+ const updateShapeById = useCallback(
+ (
+ shapeId: string,
+ newCoordinates?: any,
+ newDistance?: number,
+ newSquare?: number | null
+ ) => {
+ setUpdateShape(true);
+ setShapes((prevShapes) => {
+ return prevShapes.map((s) => {
+ if (s.shapeId === shapeId) {
+ return {
+ ...s,
+ coordinates: newCoordinates,
+ distance: newDistance,
+ area: newSquare,
+ };
+ } else {
+ return s;
+ }
+ });
+ });
+ },
+ []
+ );
+
+ const setLastVisibleShapeActive = useCallback(() => {
+ setShapes((currentShapes) => {
+ const lastShapeId = currentShapes[currentShapes.length - 1]?.shapeId;
+ if (lastShapeId) {
+ setActiveShape(lastShapeId);
+ }
+ return currentShapes;
+ });
+ }, []);
+
+ const setDrawingWithLastActiveShape = useCallback(() => {
+ setActiveShape((currentActiveShape) => {
+ if (currentActiveShape) {
+ setLastActiveShapeBeforeDrawing(currentActiveShape);
+ setDrawingShape(true);
+ }
+ return currentActiveShape;
+ });
+ }, []);
+
+ const setActiveShapeIfDrawCancelled = useCallback(() => {
+ setLastActiveShapeBeforeDrawing((lastActiveShape) => {
+ setVisibleShapes((visible) => {
+ if (lastActiveShape && visible[0]?.shapeId !== DRAWING_SHAPE_ID) {
+ setActiveShape(lastActiveShape);
+ setDrawingShape(false);
+ } else {
+ return []; // Clear visible shapes
+ }
+ return visible;
+ });
+ return lastActiveShape;
+ });
+ }, []);
+
+ const toggleMeasurementMode = useCallback(() => {
+ if (!isMeasurementEnabled) {
+ console.debug("[MapMeasurementsProvider] Enabling measurement mode");
+ setMeasurementEnabled(true);
+ } else {
+ console.debug("[MapMeasurementsProvider] Disabling measurement mode");
+ setMeasurementEnabled(false);
+ setDrawingShape(false);
+ setLastVisibleShapeActive();
+ }
+ }, [isMeasurementEnabled, setLastVisibleShapeActive]);
+
+ // NOTE: newArea is pre-formatted string like "123.45 m²" or "1.23 km²" from calculateArea()
+ const updateAreaOfDrawing = useCallback((newArea: string) => {
+ setVisibleShapes((visibleShapes) => {
+ const shape = visibleShapes.map((s) => {
+ if (s.shapeId === DRAWING_SHAPE_ID) {
+ return {
+ ...s,
+ area: newArea,
+ };
+ }
+ return s;
+ });
+ return shape;
+ });
+ }, []);
+
+ const updateTitle = useCallback(
+ (shapeId: string | number, customTitle: string) => {
+ setVisibleShapes((currentVisibleShapes) => {
+ const shapeFromVisible = currentVisibleShapes.find(
+ (s) => s.shapeId === shapeId
+ );
+
+ if (!shapeFromVisible) return currentVisibleShapes;
+ return currentVisibleShapes.map((shape) => {
+ if (shape.shapeId === shapeId) {
+ return {
+ ...shapeFromVisible,
+ customTitle,
+ };
+ }
+ return shape;
+ });
+ });
+
+ // Update all shapes - find the shape first to preserve all properties
+ setShapes((currentShapes) => {
+ const shapeFromAllShapes = currentShapes.find(
+ (s) => s.shapeId === shapeId
+ );
+ if (!shapeFromAllShapes) return currentShapes;
+
+ return currentShapes.map((shape) => {
+ if (shape.shapeId === shapeId) {
+ return {
+ ...shapeFromAllShapes,
+ customTitle,
+ };
+ }
+ return shape;
+ });
+ });
+
+ // Set update title status to trigger any necessary UI updates
+ setUpdateTitleStatus(true);
+ },
+ []
+ );
+
+ const contextValue = useMemo(
+ () => ({
+ isMeasurementEnabled,
+ setMeasurementEnabled,
+ shapes,
+ setShapes,
+ addShape,
+ activeShape,
+ setActiveShape,
+ visibleShapes,
+ setVisibleShapes,
+ showAll,
+ setShowAll,
+ deleteAll,
+ setDeleteAll,
+ drawingShape,
+ setDrawingShape,
+ lastActiveShapeBeforeDrawing,
+ setLastActiveShapeBeforeDrawing,
+ moveToShape,
+ setMoveToShape,
+ updateShape,
+ setUpdateShape,
+ mapMovingEnd,
+ setMapMovingEnd,
+ updateTitleStatus,
+ setUpdateTitleStatus,
+ deleteShapeById,
+ deleteVisibleShapeById,
+ updateShapeById,
+ setLastVisibleShapeActive,
+ setDrawingWithLastActiveShape,
+ setActiveShapeIfDrawCancelled,
+ toggleMeasurementMode,
+ updateAreaOfDrawing,
+ updateTitle,
+ isSnapping,
+ setIsSnapping,
+ config: mergedConfig,
+ }),
+ [
+ isMeasurementEnabled,
+ shapes,
+ addShape,
+ activeShape,
+ visibleShapes,
+ showAll,
+ deleteAll,
+ drawingShape,
+ lastActiveShapeBeforeDrawing,
+ moveToShape,
+ updateShape,
+ mapMovingEnd,
+ updateTitleStatus,
+ deleteShapeById,
+ deleteVisibleShapeById,
+ updateShapeById,
+ setLastVisibleShapeActive,
+ setDrawingWithLastActiveShape,
+ setActiveShapeIfDrawCancelled,
+ toggleMeasurementMode,
+ updateAreaOfDrawing,
+ updateTitle,
+ isSnapping,
+ mergedConfig,
+ ]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/libraries/commons/measurements/src/lib/context/hooks/useMapMeasurementsContext.ts b/libraries/commons/measurements/src/lib/context/hooks/useMapMeasurementsContext.ts
new file mode 100644
index 0000000000..64b5d077fe
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/context/hooks/useMapMeasurementsContext.ts
@@ -0,0 +1,17 @@
+/**
+ * useMapMeasurementsContext Hook
+ * Hook to access the map measurements context
+ */
+
+import { useContext } from "react";
+import { MapMeasurementsContext } from "../MapMeasurementsContext";
+
+export function useMapMeasurementsContext() {
+ const ctx = useContext(MapMeasurementsContext);
+ if (!ctx) {
+ throw new Error(
+ "useMapMeasurementsContext must be used within an MapMeasurementsProvider"
+ );
+ }
+ return ctx;
+}
diff --git a/libraries/commons/measurements/src/lib/context/index.ts b/libraries/commons/measurements/src/lib/context/index.ts
new file mode 100644
index 0000000000..f84f111505
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/context/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Context Exports
+ * Re-export context, provider, hook, and types
+ */
+
+export { MapMeasurementsContext } from "./MapMeasurementsContext";
+export type { MapMeasurementsContextType } from "./MapMeasurementsContext";
+export {
+ MapMeasurementsProvider,
+ defaultConfig,
+} from "./MapMeasurementsProvider";
+export { useMapMeasurementsContext } from "./hooks/useMapMeasurementsContext";
+export type {
+ ActiveShape,
+ MeasurementConfig,
+ MeasurementMapStatus,
+ PartialMeasurementConfig,
+} from "./MapMeasurementsContext.d";
diff --git a/libraries/commons/measurements/src/lib/hooks/useMeasurements.ts b/libraries/commons/measurements/src/lib/hooks/useMeasurements.ts
new file mode 100644
index 0000000000..7ad6526827
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/hooks/useMeasurements.ts
@@ -0,0 +1,1059 @@
+import React, { useState, useEffect, useContext, useRef } from "react";
+
+import { Map as MapLibreMap } from "maplibre-gl";
+
+import L from "leaflet";
+import "leaflet-draw";
+import "leaflet-editable";
+
+import { TopicMapContext } from "react-cismap/contexts/TopicMapContextProvider";
+
+import "../utils/measure";
+import { DRAWING_SHAPE_ID } from "../utils/constants";
+import { MeasureControl } from "../leaflet-control-measurement-extension";
+import useDeviceDetection from "../hooks/useDeviceDetection";
+import { useMapMeasurementsContext } from "../context";
+import {
+ toLatLngFromClosestPoint,
+ isCoordMatchLatLng,
+ isFirstVertexMatch,
+ tryClosePolygon,
+ distanceBetweenLatLng,
+ screenPixelDistance,
+ pixelRadiusToMeters,
+ createSnappingIndicator,
+ findClosestSnappingPoint,
+ SNAPPING_MODIFIER_KEY,
+ handleDuplicateVertex,
+ createSnappingFeature,
+} from "../utils/snapping";
+import { filterArrByIds, findLargestNumber } from "../utils/shapes";
+import { SnappingPoint } from "./../types";
+import { TOOLTIP_LABELS } from "../labels";
+import { extractPointsFromMeasurementShape } from "../snapping/utils/coordinateExtraction";
+import { getSnappingPointsFromMapLibre } from "../snapping/utils/mapLibreExtraction";
+
+import "../styles/m-style.css";
+import "leaflet/dist/leaflet.css";
+import "leaflet-draw/dist/leaflet.draw.css";
+import "leaflet-measure-path/leaflet-measure-path.css";
+
+export interface MeasurementShapeDrawing {
+ shapeId: number | string;
+ number: number;
+ coordinates?: unknown;
+ [key: string]: unknown;
+}
+
+export const useMeasurements = (snappingLayers: MapLibreMap[] = []) => {
+ const { realRoutedMapRef } =
+ useContext(TopicMapContext);
+ const {
+ isMeasurementEnabled,
+ activeShape,
+ setActiveShape,
+ shapes,
+ setShapes,
+ addShape,
+ deleteAll,
+ setDeleteAll,
+ setUpdateShape,
+ visibleShapes,
+ setVisibleShapes,
+ drawingShape: ifDrawing,
+ setDrawingShape,
+ moveToShape,
+ setMoveToShape,
+ showAll,
+ setShowAll,
+ toggleMeasurementMode: toggleUIMode,
+ setMapMovingEnd,
+ deleteShapeById,
+ updateShapeById,
+ setLastVisibleShapeActive,
+ setDrawingWithLastActiveShape,
+ setActiveShapeIfDrawCancelled,
+ updateAreaOfDrawing,
+ deleteVisibleShapeById,
+ config,
+ } = useMapMeasurementsContext();
+
+ // Local state for drawing logic
+ const [status, setStatus] = useState("INACTIVE");
+ const currentDrawHandlerRef = useRef(null);
+ const snappingLatlngRef = useRef(null);
+ const [measureControl, setMeasureControl] = useState(null);
+
+ // Helper to update status
+ const setCurrentDrawHandler = (handler: any) => {
+ currentDrawHandlerRef.current = handler;
+ };
+
+ // destructure config for snapping
+ const {
+ snappingQueryRadius,
+ snappingMinZoom,
+ snappingOnUpdate,
+ snappingRadiusVisible,
+ snappingIdentityDistanceMeters,
+ } = config;
+
+ const queryRadiusRef = useRef(snappingQueryRadius);
+ const circleMarkerRef = useRef(null);
+ const snappingIndicatorRef = useRef(null); // Leaflet marker for snapping point
+ const shapesRef = useRef(shapes);
+
+ const snappingLayersRef = useRef(snappingLayers);
+ const lastHoveredMarkerRef = useRef(null);
+ const isDraggingVertexRef = useRef(false);
+ const lastSnappedCoordRef = useRef<[number, number] | null>(null);
+ const statusRef = useRef(status);
+ const lastVertexCountRef = useRef(0);
+
+ const cachedShapePointsRef = useRef([]);
+
+ useEffect(() => {
+ snappingLayersRef.current = snappingLayers;
+ }, [snappingLayers]);
+
+ useEffect(() => {
+ shapesRef.current = shapes;
+ // Update cache for snapping points from shapes
+ const points: SnappingPoint[] = [];
+ shapes.forEach((shape: any) => {
+ // Filter out the currently active shape to avoid ghost snapping to its old state
+ // The active shape's current points are handled by the drawing-in-progress logic
+ if (shape.shapeId !== activeShape) {
+ points.push(
+ ...extractPointsFromMeasurementShape(shape, "measurements")
+ );
+ }
+ });
+ cachedShapePointsRef.current = points;
+ }, [shapes, activeShape]);
+
+ useEffect(() => {
+ queryRadiusRef.current = snappingQueryRadius;
+ }, [snappingQueryRadius]);
+
+ useEffect(() => {
+ statusRef.current = status;
+ }, [status]);
+
+ useEffect(() => {
+ const leafletMap = realRoutedMapRef.current?.leafletMap?.leafletElement;
+
+ if (
+ isMeasurementEnabled &&
+ leafletMap &&
+ typeof leafletMap.on === "function"
+ ) {
+ // Import L from leaflet
+ let nextVertexCandidate: SnappingPoint | null = null;
+ const nextVertexCandidateRef = { current: null as SnappingPoint | null }; // Stable ref
+
+ // Centralized cleanup for all snapping state
+ const clearSnapping = () => {
+ try {
+ if (circleMarkerRef.current) {
+ leafletMap.removeLayer(circleMarkerRef.current);
+ circleMarkerRef.current = null;
+ }
+ if (snappingIndicatorRef.current) {
+ leafletMap.removeLayer(snappingIndicatorRef.current);
+ snappingIndicatorRef.current = null;
+ }
+ // Clear cursor on all MapLibre maps
+ snappingLayers.forEach((map) => {
+ if (map && map.getCanvas) {
+ map.getCanvas().style.cursor = "";
+ }
+ });
+ // Clear all snapping refs
+ nextVertexCandidate = null;
+ nextVertexCandidateRef.current = null;
+ snappingLatlngRef.current = null;
+ if (measureControl) {
+ measureControl.options.snappingLatlng = null;
+ }
+ } catch (_) {
+ // no-op safeguard
+ }
+ };
+
+ // Store last mouse event to re-trigger handlers on key press
+ const lastMouseEventRef = { current: null as any };
+
+ const updateTooltipTemplate = (isPressed: boolean) => {
+ const snappingText = isPressed
+ ? TOOLTIP_LABELS.snapping.inactive
+ : TOOLTIP_LABELS.snapping.active;
+
+ if (
+ L.drawLocal &&
+ L.drawLocal.draw &&
+ L.drawLocal.draw.handlers &&
+ L.drawLocal.draw.handlers.polyline
+ ) {
+ L.drawLocal.draw.handlers.polyline.tooltip.start = `${TOOLTIP_LABELS.measurement.start}
${snappingText}`;
+ L.drawLocal.draw.handlers.polyline.tooltip.cont = `${TOOLTIP_LABELS.measurement.continue}
${snappingText}`;
+ }
+ };
+
+ const mousemoveHandler = (e: MouseEvent) => {
+ // Track vertex count to detect if Leaflet Draw adds a vertex before our click handler
+ // This helps prevents premature line finishing when snapping adds a new vertex
+ if (currentDrawHandlerRef.current?._markers) {
+ lastVertexCountRef.current =
+ currentDrawHandlerRef.current._markers.length;
+ }
+
+ // Prevent infinite loop from synthetic events we generate for snapping
+ if ((e as any)._isSyntheticSnapped) {
+ return;
+ }
+
+ // If mouse button is pressed (e.g. panning), do not snap
+ if (e.buttons !== 0) {
+ if (!isDraggingVertexRef.current) {
+ clearSnapping();
+ }
+ return;
+ }
+
+ lastMouseEventRef.current = e;
+
+ // Update tooltip text based on Snapping Modifier Key
+ const isPressed = e.getModifierState(SNAPPING_MODIFIER_KEY);
+ updateTooltipTemplate(isPressed);
+
+ // Force update of current tooltip if it exists
+ // Removed to prevent clearing distance
+
+ // Skip snapping indicator during vertex drag if snappingOnUpdate is disabled
+ if (isDraggingVertexRef.current && !snappingOnUpdate) {
+ clearSnapping();
+ return;
+ }
+
+ // Check if Snapping Modifier Key is pressed - if so, disable snapping temporarily
+ if (isPressed) {
+ clearSnapping();
+ return;
+ }
+
+ // Check zoom level - only work if zoom >= configured minimum
+ const currentZoom = leafletMap.getZoom();
+
+ // Remove old circle if exists
+ if (circleMarkerRef.current) {
+ leafletMap.removeLayer(circleMarkerRef.current);
+ }
+
+ const currentSnappingLayers = snappingLayersRef.current;
+
+ // Get mouse position in lat/lng using Leaflet (always available)
+ const mouseLatLng = leafletMap.mouseEventToLatLng(e);
+ const mousePoint = leafletMap.latLngToContainerPoint(mouseLatLng);
+
+ const currentRadius = queryRadiusRef.current;
+
+ // Show radius circle if enabled and in WAITING or DRAWING status
+ if (
+ snappingRadiusVisible &&
+ (statusRef.current === "WAITING" || statusRef.current === "DRAWING")
+ ) {
+ const radiusInMeters = pixelRadiusToMeters(
+ currentRadius,
+ mouseLatLng.lat,
+ currentZoom
+ );
+
+ circleMarkerRef.current = L.circle(mouseLatLng, {
+ radius: radiusInMeters,
+ color: "#ffffff",
+ fillColor: "#ffffff",
+ fillOpacity: 0.15,
+ weight: 1,
+ opacity: 0.4,
+ interactive: false,
+ }).addTo(leafletMap);
+ }
+ const coordinatePoints: SnappingPoint[] = [];
+
+ // 1. Extract from vector features (loop through all MapLibre maps)
+ // Only extract if zoom is high enough
+ if (currentZoom >= snappingMinZoom) {
+ coordinatePoints.push(
+ ...getSnappingPointsFromMapLibre(
+ currentSnappingLayers,
+ { x: e.clientX, y: e.clientY },
+ currentRadius
+ )
+ );
+ }
+
+ // 2. Extract from measurement shapes (independent of MapLibre)
+ // Use cached points
+ coordinatePoints.push(...cachedShapePointsRef.current);
+
+ // 3. Extract from in-progress drawing (if currently drawing)
+ const currentDrawHandlerValue = currentDrawHandlerRef.current;
+ if (
+ currentDrawHandlerValue &&
+ currentDrawHandlerValue._poly &&
+ currentDrawHandlerValue._poly._latlngs
+ ) {
+ const drawingLatLngs = currentDrawHandlerValue._poly._latlngs;
+ drawingLatLngs.forEach((latlng: any) => {
+ coordinatePoints.push({
+ coordinates: [latlng.lng, latlng.lat],
+ sourceId: "drawing-in-progress",
+ });
+ });
+ }
+
+ // Filter points to only those within the query radius and calculate distances
+ // Find closest snapping point using helper
+ const projectToScreen = (coord: [number, number]) => {
+ const pointLatLng = L.latLng(coord[1], coord[0]);
+ return leafletMap.latLngToContainerPoint(pointLatLng);
+ };
+
+ const closestResult = findClosestSnappingPoint(
+ coordinatePoints,
+ mousePoint,
+ currentRadius,
+ projectToScreen
+ );
+
+ // Determine snapping point
+ const isSnapped = !!closestResult;
+ const snappedFeature: any = createSnappingFeature(
+ closestResult,
+ mouseLatLng
+ );
+
+ nextVertexCandidate = snappedFeature;
+ // Store candidate (snapped or unsnapped)
+ nextVertexCandidateRef.current = snappedFeature;
+
+ const finalLatLng = toLatLngFromClosestPoint(nextVertexCandidate);
+ // Logic for updating snappingLatlng
+ let newSnappingLatlng = null;
+
+ if (finalLatLng) {
+ // Check if we're snapping to the first vertex of an in-progress drawing
+ // If so, only allow snapping if we're within the query radius (prevents premature polygon closure)
+ let shouldSnap = true;
+
+ if (
+ isFirstVertexMatch(
+ currentDrawHandlerValue,
+ finalLatLng,
+ snappingIdentityDistanceMeters
+ )
+ ) {
+ // We're trying to snap to first vertex - check pixel distance from mouse
+ const map = realRoutedMapRef.current?.leafletMap?.leafletElement;
+ const firstVertex = currentDrawHandlerValue._poly._latlngs[0];
+ if (map && mouseLatLng) {
+ const mousePt = map.latLngToContainerPoint(mouseLatLng);
+ const vertexPt = map.latLngToContainerPoint(firstVertex);
+ const pixelDist = screenPixelDistance(mousePt, vertexPt);
+
+ // Only snap if mouse is within query radius
+ if (pixelDist > queryRadiusRef.current) {
+ shouldSnap = false;
+ }
+ }
+ }
+
+ if (shouldSnap) {
+ newSnappingLatlng = finalLatLng;
+ }
+ }
+
+ // Update ref and control directly
+ snappingLatlngRef.current = newSnappingLatlng;
+ if (measureControl) {
+ measureControl.options.snappingLatlng = newSnappingLatlng;
+ }
+
+ // Trigger vertex marker hover for tooltip/area preview (Phase 3)
+ // Check if we snapped to the first vertex of in-progress drawing
+ if (
+ isSnapped &&
+ currentDrawHandlerValue &&
+ currentDrawHandlerValue._markers &&
+ closestResult
+ ) {
+ const snappedCoord = closestResult.point.coordinates;
+
+ // Check if snapped point matches first vertex coordinates (regardless of source)
+ // This handles both drawing-in-progress points AND vector features at same location
+ const firstVertex = currentDrawHandlerValue._poly?._latlngs?.[0];
+ if (
+ firstVertex &&
+ isCoordMatchLatLng(
+ snappedCoord,
+ firstVertex,
+ snappingIdentityDistanceMeters
+ )
+ ) {
+ // Fire mouseover on first vertex marker to show area preview
+ const firstMarker = currentDrawHandlerValue._markers[0];
+ if (firstMarker && lastHoveredMarkerRef.current !== firstMarker) {
+ lastHoveredMarkerRef.current = firstMarker;
+ firstMarker.fire("mouseover", { target: firstMarker });
+ }
+ } else {
+ // Snapped to different point - fire mouseout
+ if (lastHoveredMarkerRef.current) {
+ lastHoveredMarkerRef.current.fire("mouseout", {
+ target: lastHoveredMarkerRef.current,
+ });
+ lastHoveredMarkerRef.current = null;
+ }
+ }
+ } else {
+ // Not snapped or no drawing - fire mouseout if we were hovering
+ if (lastHoveredMarkerRef.current) {
+ lastHoveredMarkerRef.current.fire("mouseout", {
+ target: lastHoveredMarkerRef.current,
+ });
+ lastHoveredMarkerRef.current = null;
+ }
+ }
+
+ // Only update indicator if snap position changed
+ const currentCoord: [number, number] | null =
+ finalLatLng && isSnapped ? [finalLatLng.lng, finalLatLng.lat] : null;
+
+ const lastCoord = lastSnappedCoordRef.current;
+ const coordChanged =
+ !lastCoord ||
+ !currentCoord ||
+ Math.abs(lastCoord[0] - currentCoord[0]) > 0.00001 ||
+ Math.abs(lastCoord[1] - currentCoord[1]) > 0.00001;
+
+ if (coordChanged) {
+ // Remove old snapping indicator if exists
+ if (snappingIndicatorRef.current) {
+ leafletMap.removeLayer(snappingIndicatorRef.current);
+ snappingIndicatorRef.current = null;
+ }
+
+ // Create Leaflet marker for snapping indicator ONLY when snapped
+ if (
+ finalLatLng &&
+ isSnapped &&
+ (statusRef.current === "WAITING" ||
+ statusRef.current === "DRAWING" ||
+ statusRef.current === "INACTIVE")
+ ) {
+ snappingIndicatorRef.current = createSnappingIndicator(
+ finalLatLng,
+ leafletMap
+ );
+ }
+
+ lastSnappedCoordRef.current = currentCoord;
+ }
+
+ // Dispatch synthetic event for Leaflet Draw if snapped
+ if (isSnapped && finalLatLng && !isPressed) {
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+
+ const mapContainer = leafletMap.getContainer();
+ const rect = mapContainer.getBoundingClientRect();
+ const point = leafletMap.latLngToContainerPoint(finalLatLng);
+
+ const newEvent = new MouseEvent("mousemove", {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ clientX: rect.left + point.x,
+ clientY: rect.top + point.y,
+ screenX: e.screenX,
+ screenY: e.screenY,
+ altKey: e.altKey,
+ ctrlKey: e.ctrlKey,
+ shiftKey: e.shiftKey,
+ metaKey: e.metaKey,
+ buttons: e.buttons,
+ });
+ (newEvent as any)._isSyntheticSnapped = true;
+ mapContainer.dispatchEvent(newEvent);
+ }
+ };
+
+ const mouseoutHandler = () => {
+ // Remove circle and snapping indicator when mouse leaves map
+ if (circleMarkerRef.current) {
+ leafletMap.removeLayer(circleMarkerRef.current);
+ circleMarkerRef.current = null;
+ }
+ if (snappingIndicatorRef.current) {
+ leafletMap.removeLayer(snappingIndicatorRef.current);
+ snappingIndicatorRef.current = null;
+ }
+ };
+
+ const container = leafletMap.getContainer();
+ container.addEventListener("mousemove", mousemoveHandler, {
+ capture: true,
+ });
+ leafletMap.on("mouseout", mouseoutHandler);
+
+ // Shared helper for vertex drag snapping
+ const findVertexSnapTarget = (vertexLatLng: {
+ lat: number;
+ lng: number;
+ }): SnappingPoint | null => {
+ const vertexPoint = leafletMap.latLngToContainerPoint(vertexLatLng);
+ const currentRadius = queryRadiusRef.current;
+ const coordinatePoints: SnappingPoint[] = [];
+ const currentZoom = leafletMap.getZoom();
+
+ // Extract snap points from vector features
+ if (currentZoom >= snappingMinZoom) {
+ const mapContainer = leafletMap.getContainer();
+ const mapRect = mapContainer.getBoundingClientRect();
+ const screenX = vertexPoint.x + mapRect.left;
+ const screenY = vertexPoint.y + mapRect.top;
+
+ coordinatePoints.push(
+ ...getSnappingPointsFromMapLibre(
+ snappingLayersRef.current,
+ { x: screenX, y: screenY },
+ currentRadius
+ )
+ );
+ }
+
+ // Extract from measurement shapes
+ coordinatePoints.push(...cachedShapePointsRef.current);
+
+ // Filter out self (exclude self-snapping)
+ const filtered = coordinatePoints.filter((point) => {
+ const pointLatLng = {
+ lat: point.coordinates[1],
+ lng: point.coordinates[0],
+ };
+ return (
+ distanceBetweenLatLng(pointLatLng, vertexLatLng) >=
+ snappingIdentityDistanceMeters
+ );
+ });
+
+ // Find closest
+ const projectToScreen = (coord: [number, number]) => {
+ const pointLatLng = L.latLng(coord[1], coord[0]);
+ return leafletMap.latLngToContainerPoint(pointLatLng);
+ };
+
+ const result = findClosestSnappingPoint(
+ filtered,
+ vertexPoint,
+ currentRadius,
+ projectToScreen
+ );
+
+ return result?.point ?? null;
+ };
+
+ // Phase 4: Show snap indicator during vertex drag
+ const vertexDragHandler = (e: any) => {
+ isDraggingVertexRef.current = true;
+ if (!snappingOnUpdate) return;
+
+ const vertex = e.vertex;
+ if (!vertex) return;
+
+ // Remove old indicator
+ if (snappingIndicatorRef.current) {
+ leafletMap.removeLayer(snappingIndicatorRef.current);
+ snappingIndicatorRef.current = null;
+ }
+
+ const snapTarget = findVertexSnapTarget(vertex.latlng);
+ if (snapTarget) {
+ snappingIndicatorRef.current = createSnappingIndicator(
+ { lat: snapTarget.coordinates[1], lng: snapTarget.coordinates[0] },
+ leafletMap
+ );
+ }
+ };
+
+ leafletMap.on("editable:vertex:drag", vertexDragHandler);
+
+ // Phase 4: Snap vertex AFTER drag ends
+ const vertexDragEndHandler = (e: any) => {
+ isDraggingVertexRef.current = false;
+ if (!snappingOnUpdate) return;
+
+ const vertex = e.vertex;
+ if (!vertex) return;
+
+ const snapTarget = findVertexSnapTarget(vertex.latlng);
+ if (snapTarget) {
+ // Snap vertex to final position
+ vertex.latlng.lat = snapTarget.coordinates[1];
+ vertex.latlng.lng = snapTarget.coordinates[0];
+ vertex.update();
+ // Force complete refresh of the editor
+ if (e.layer.editor) {
+ e.layer.editor.reset();
+ }
+ e.layer.redraw();
+ }
+ };
+
+ leafletMap.on("editable:vertex:dragend", vertexDragEndHandler);
+
+ // click handler for snapped vertices
+ const mapContainer = leafletMap.getContainer();
+ const clickHandler = (event: MouseEvent) => {
+ const drawHandler = currentDrawHandlerRef.current;
+ if (!drawHandler || !drawHandler.addVertex) return; // Not drawing
+
+ const candidate = nextVertexCandidateRef.current;
+ if (!candidate) return; // Should not happen if mouse on map
+
+ // Get latlng from candidate (snapped or unsnapped)
+ const snappedLatlng = toLatLngFromClosestPoint(candidate);
+
+ if (!snappedLatlng) return;
+
+ // Check if vertex count increased (meaning Leaflet Draw already added the vertex)
+ // If so, abort to prevent premature finishing or double-adding
+ const currentVertexCount =
+ drawHandler._markers && drawHandler._markers.length
+ ? drawHandler._markers.length
+ : 0;
+
+ if (currentVertexCount > lastVertexCountRef.current) {
+ return;
+ }
+
+ // Check if snapping to first vertex (polygon closure)
+ // Use snapped position - snapping to first vertex should close the polygon
+ if (
+ isFirstVertexMatch(
+ drawHandler,
+ snappedLatlng,
+ snappingIdentityDistanceMeters
+ )
+ ) {
+ // Check if handler is still valid/enabled before trying to close
+ // Leaflet.Draw might have already closed it via addVertex override
+ if (!drawHandler._enabled) {
+ console.debug(
+ "[useMeasurements] Handler disabled - aborting tryClosePolygon"
+ );
+ return;
+ }
+
+ // Explicitly set polygon mode when closing via snap
+ if (measureControl && measureControl.options) {
+ console.debug(
+ "[useMeasurements] Closing polygon via snap - setting shapeMode to 'polygon'"
+ );
+ measureControl.options.shapeMode = "polygon";
+ }
+
+ try {
+ if (tryClosePolygon(drawHandler)) {
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+ return;
+ }
+ } catch (e) {
+ console.warn(
+ "[useMeasurements] Error closing polygon (likely already closed):",
+ e
+ );
+ }
+ }
+
+ // Check for duplicate of LAST vertex
+ // This covers both "finish line" (on 2+ points) and "prevent duplicate start" (on 1 point)
+ if (
+ handleDuplicateVertex(
+ drawHandler,
+ snappedLatlng,
+ snappingIdentityDistanceMeters
+ )
+ ) {
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+ return;
+ }
+
+ // Directly add vertex at snapped position
+ drawHandler.addVertex(snappedLatlng);
+ // Clear snap ref after adding vertex to prevent stale snap on next click
+ nextVertexCandidateRef.current = null;
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+ };
+ mapContainer.addEventListener("click", clickHandler, true);
+
+ // Keydown/keyup handlers for ALT key
+ const handleKeyToggle = (isPressed: boolean) => {
+ updateTooltipTemplate(isPressed);
+
+ if (lastMouseEventRef.current) {
+ const mapContainer = leafletMap.getContainer();
+ const originalEvent = lastMouseEventRef.current;
+
+ // Dispatch synthetic event to trigger Leaflet Draw update
+ const newEvent = new MouseEvent("mousemove", {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ clientX: originalEvent.clientX,
+ clientY: originalEvent.clientY,
+ screenX: originalEvent.screenX,
+ screenY: originalEvent.screenY,
+ altKey: isPressed,
+ ctrlKey: originalEvent.ctrlKey,
+ shiftKey: originalEvent.shiftKey,
+ metaKey: originalEvent.metaKey,
+ buttons: originalEvent.buttons,
+ });
+
+ mapContainer.dispatchEvent(newEvent);
+ }
+ };
+
+ const keydownHandler = (e: KeyboardEvent) => {
+ if (e.key === SNAPPING_MODIFIER_KEY) {
+ handleKeyToggle(true);
+ }
+ };
+
+ const keyupHandler = (e: KeyboardEvent) => {
+ if (e.key === SNAPPING_MODIFIER_KEY) {
+ handleKeyToggle(false);
+ }
+ };
+
+ document.addEventListener("keydown", keydownHandler);
+ document.addEventListener("keyup", keyupHandler);
+
+ // Cleanup function to remove listeners and markers
+ return () => {
+ leafletMap
+ .getContainer()
+ .removeEventListener("mousemove", mousemoveHandler, true);
+ leafletMap.off("mouseout", mouseoutHandler);
+ leafletMap.off("editable:vertex:drag", vertexDragHandler);
+ leafletMap.off("editable:vertex:dragend", vertexDragEndHandler);
+ mapContainer.removeEventListener("click", clickHandler, true);
+ document.removeEventListener("keydown", keydownHandler);
+ document.removeEventListener("keyup", keyupHandler);
+ if (circleMarkerRef.current) {
+ leafletMap.removeLayer(circleMarkerRef.current);
+ circleMarkerRef.current = null;
+ }
+ if (snappingIndicatorRef.current) {
+ leafletMap.removeLayer(snappingIndicatorRef.current);
+ snappingIndicatorRef.current = null;
+ }
+ };
+ }
+ }, [
+ realRoutedMapRef,
+ snappingMinZoom,
+ snappingOnUpdate,
+ snappingRadiusVisible,
+ snappingIdentityDistanceMeters,
+ snappingLayers,
+ isMeasurementEnabled,
+ measureControl,
+ ]);
+
+ const [visiblePolylines, setVisiblePolylines] = useState<(string | number)[]>(
+ []
+ );
+ const [drawingShape, setDrawingLine] = useState(null);
+
+ // Track last valid state for recovery
+ const lastValidStateRef = React.useRef<{
+ activeShape: any;
+ mode: string;
+ wasDrawing: boolean;
+ } | null>(null);
+
+ const device = useDeviceDetection();
+
+ const toggleMeasurementModeHandler = () => {
+ toggleUIMode();
+ };
+
+ useEffect(() => {
+ const leafletMap = realRoutedMapRef.current?.leafletMap?.leafletElement;
+ if (leafletMap && !measureControl) {
+ const mapExample = leafletMap;
+
+ console.debug(
+ "[Measurements] Initializing measurement control with valid map"
+ );
+
+ const customOptions = {
+ position: "topright",
+ // icon_lineActive: makeMeasureActiveIcon,
+ // icon_lineInactive: makeMeasureIcon,
+ // icon_polygonActive: polygonActiveIcon,
+ // icon_polygonInactive: polygonIcon,
+ activeShape,
+ mode_btn: ``,
+ msj_disable_tool: TOOLTIP_LABELS.general.disableTool,
+ device,
+ shapes,
+ snappingLatlng: snappingLatlngRef?.current,
+ snappingEnabled: true,
+ cbSaveShape: saveShapeHandler,
+ cbUpdateShape: updateShapeHandler,
+ cbDeleteShape: deleteShapeHandler,
+ cbDeleteVisibleShapeById: deleteVisibleShapeByIdHandler,
+ cbVisiblePolylinesChange: visiblePolylinesChange,
+ cbSetDrawingStatus: drawingStatusHandler,
+ cbSetDrawingShape: drawingShapeHandler,
+ measurementOrder: findLargestNumber(shapes),
+ enabled: isMeasurementEnabled,
+ cbSetActiveShape: setActiveShapeHandler,
+ cbSetUpdateStatusHandler: setUpdateStatusHandler,
+ cbMapMovingEndHandler: mapMovingEndHandler,
+ cbSaveLastActiveShapeIdBeforeDrawingHandler:
+ saveLastActiveShapeIdBeforeDrawingHandler,
+ cbChangeActiveCancelledShapeId: changeActiveCancelledShapeId,
+ cbToggleMeasurementMode: toggleMeasurementModeHandler,
+ cbUpdateAreaOfDrawingMeasurement: updateAreaOfDrawingMeasurementHandler,
+ cbSetCurrentDrawHandler: setCurrentDrawHandler,
+ cbSetMapStatus: setStatus,
+ };
+
+ const measurePolygonControl = new (MeasureControl as any)(customOptions);
+ measurePolygonControl.addTo(mapExample);
+
+ setMeasureControl(measurePolygonControl);
+
+ // Restore previous state if available
+ if (lastValidStateRef.current) {
+ console.debug(
+ "[Measurements] Restoring previous state:",
+ lastValidStateRef.current
+ );
+ const savedState = lastValidStateRef.current;
+
+ // Restore mode if it was in measurement mode
+ if (savedState.mode === "measurement" && !isMeasurementEnabled) {
+ // Mode will be restored by parent component
+ }
+
+ // Restore active shape if there was one
+ if (savedState.activeShape && !savedState.wasDrawing) {
+ setTimeout(() => {
+ setActiveShape(savedState.activeShape);
+ }, 100);
+ }
+
+ lastValidStateRef.current = null;
+ }
+ }
+ }, [realRoutedMapRef]);
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ if (measureControl) {
+ console.debug("[Measurements] Cleaning up control on unmount");
+ try {
+ const mapExample =
+ realRoutedMapRef.current?.leafletMap?.leafletElement;
+ if (mapExample) {
+ mapExample.removeControl(measureControl);
+ }
+ } catch (e) {
+ console.warn("[Measurements] Error during cleanup:", e);
+ }
+ }
+ };
+ }, [measureControl, realRoutedMapRef]);
+
+ // Sync device detection to control options
+ useEffect(() => {
+ if (measureControl) {
+ measureControl.options.device = device;
+ }
+ }, [measureControl, device]);
+
+ useEffect(() => {
+ if (measureControl && activeShape) {
+ const shapeCoordinates = shapes.filter((s) => s.shapeId === activeShape);
+ const map = realRoutedMapRef.current?.leafletMap?.leafletElement;
+ if (!map) return;
+
+ if (ifDrawing) {
+ setMoveToShape(null);
+ }
+
+ if (shapeCoordinates[0]?.shapeId && !ifDrawing && !deleteAll) {
+ measureControl.changeColorByActivePolyline(
+ map,
+ shapeCoordinates[0].shapeId
+ );
+ }
+ if (showAll) {
+ const allPolylines = measureControl.getAllPolylines(map);
+ measureControl.fitMapToPolylines(map, allPolylines);
+ setShowAll(false);
+ }
+
+ if (deleteAll) {
+ setMoveToShape(null);
+ measureControl.removePolylineById(map, activeShape);
+ const cleanArr = visibleShapes.filter((m) => m.shapeId !== activeShape);
+ deleteShapeHandler(activeShape);
+ setVisibleShapes(cleanArr);
+
+ const cleanAllArr = shapes.filter((m) => m.shapeId !== activeShape);
+ setShapes(cleanAllArr);
+ setDeleteAll(false);
+ if (measureControl.options.shapes.length === 1) {
+ measureControl.options.shapes = [];
+ }
+ const cleanLocalLefletShapes = measureControl.options.shapes.filter(
+ (m) => m.shapeId !== activeShape
+ );
+
+ measureControl.options.shapes = cleanLocalLefletShapes;
+ }
+ if (moveToShape && !deleteAll) {
+ if (shapeCoordinates.length > 0) {
+ measureControl.showActiveShape(map, shapeCoordinates[0]?.coordinates);
+ }
+ }
+ }
+
+ if (measureControl) {
+ const map = realRoutedMapRef.current?.leafletMap?.leafletElement;
+ if (!map) return;
+ measureControl.setMeasurementEnabled(isMeasurementEnabled, map);
+ const shapeCoordinates = shapes.filter((s) => s.shapeId === activeShape);
+ if (shapeCoordinates[0]?.shapeId) {
+ measureControl.changeColorByActivePolyline(
+ map,
+ shapeCoordinates[0].shapeId
+ );
+ }
+
+ if (isMeasurementEnabled && visibleShapes.length === 0) {
+ measureControl.getVisibleShapeIdsArr(measureControl._map);
+ }
+ }
+ }, [
+ activeShape,
+ measureControl,
+ showAll,
+ deleteAll,
+ ifDrawing,
+ moveToShape,
+ isMeasurementEnabled,
+ realRoutedMapRef,
+ ]);
+
+ useEffect(() => {
+ if (measureControl) {
+ const cleanedVisibleArr = filterArrByIds(visiblePolylines, shapes);
+
+ // Preserve drawing shape (DRAWING_SHAPE_ID) if we're in drawing mode
+ const drawingShapeInVisible = visibleShapes.find(
+ (s) => s.shapeId === DRAWING_SHAPE_ID
+ );
+ if (
+ ifDrawing &&
+ drawingShapeInVisible &&
+ !cleanedVisibleArr.find((s) => s.shapeId === DRAWING_SHAPE_ID)
+ ) {
+ cleanedVisibleArr.push(drawingShapeInVisible);
+ }
+
+ setVisibleShapes(cleanedVisibleArr);
+ measureControl.changeMeasurementsArr(shapes);
+ }
+ }, [visiblePolylines, shapes, ifDrawing]);
+
+ useEffect(() => {
+ if (drawingShape) {
+ const cleanArr = visibleShapes.filter(
+ (m) => m.shapeId !== DRAWING_SHAPE_ID
+ );
+ setVisibleShapes([...cleanArr, drawingShape]);
+ } else {
+ setLastVisibleShapeActive();
+ }
+ }, [drawingShape]);
+
+ const saveShapeHandler = (layer) => {
+ addShape(layer);
+ };
+ const deleteShapeHandler = (id) => {
+ deleteShapeById(id);
+ };
+ const deleteVisibleShapeByIdHandler = (id) => {
+ deleteVisibleShapeById(id);
+ };
+ const updateShapeHandler = (id, newCoordinates, newDistance, newSquare) => {
+ updateShapeById(id, newCoordinates, newDistance, newSquare);
+ };
+
+ const saveLastActiveShapeIdBeforeDrawingHandler = () => {
+ setDrawingWithLastActiveShape();
+ };
+ const changeActiveCancelledShapeId = () => {
+ setActiveShapeIfDrawCancelled();
+ };
+
+ const visiblePolylinesChange = (arr) => {
+ setVisiblePolylines(arr);
+ };
+
+ const drawingStatusHandler = (status) => {
+ setDrawingShape(status);
+ };
+
+ const drawingShapeHandler = (draw) => {
+ setDrawingLine(draw);
+ };
+ const setActiveShapeHandler = (id) => {
+ setActiveShape(id);
+ setMoveToShape(null);
+ };
+ const setUpdateStatusHandler = (status) => {
+ setUpdateShape(status);
+ };
+ const mapMovingEndHandler = (status) => {
+ setMapMovingEnd(status);
+ };
+
+ const updateAreaOfDrawingMeasurementHandler = (newArea) => {
+ updateAreaOfDrawing(newArea);
+ };
+
+ // Debug: Log what's causing rerenders
+ useEffect(() => {
+ console.debug("[Measurements] Rerender triggered. State:", {
+ activeShape,
+ shapesCount: shapes.length,
+ visibleShapesCount: visibleShapes.length,
+ isMeasurementEnabled,
+ ifDrawing,
+ snappingLatlng: !!snappingLatlngRef?.current,
+ });
+ });
+};
diff --git a/libraries/commons/measurements/src/lib/labels.ts b/libraries/commons/measurements/src/lib/labels.ts
new file mode 100644
index 0000000000..da7397852a
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/labels.ts
@@ -0,0 +1,27 @@
+export const TOOLTIP_LABELS = {
+ snapping: {
+ active: "Snapping aktiv (Alt zum Deaktivieren)",
+ inactive: "Snapping deaktiviert",
+ },
+ measurement: {
+ start: "Klicken, um den Startpunkt der Messung zu setzen.",
+ continue:
+ "Klicken (ggf. mehrmals), um die nächsten Punkte des Linienzuges zu setzen.",
+ finishLine: "Zum Beenden auf den letzten angelegten Punkt klicken.",
+ finishPolygon:
+ "Zum Messen einer Fläche auf den ersten angelegten Punkt klicken und die Fläche so schließen.",
+ finishLineHover:
+ "Den Endpunkt erneut anklicken,
um die Streckenmessung zu beenden.",
+ finishPolygonHover:
+ "Den Endpunkt erneut anklicken, um die Streckenmessung zu beenden.
Zum Messen einer Fläche erneut auf den Startpunkt klicken.",
+ finishLineSimple:
+ "Den Endpunkt erneut anklicken,
um die Streckenmessung zu beenden.",
+ },
+ general: {
+ disableTool: "Möchten Sie das Tool deaktivieren?",
+ results: "Ergebnisse",
+ area: "Fläche",
+ perimeter: "Umfang",
+ measurementMode: "Messmodus",
+ },
+};
diff --git a/libraries/commons/measurements/src/lib/leaflet-control-measurement-extension/MeasureControl.ts b/libraries/commons/measurements/src/lib/leaflet-control-measurement-extension/MeasureControl.ts
new file mode 100644
index 0000000000..dd0d2ca70d
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/leaflet-control-measurement-extension/MeasureControl.ts
@@ -0,0 +1,1484 @@
+// Create a class for the plugin
+import {
+ Control,
+ DomUtil,
+ DomEvent,
+ Browser,
+ point,
+ latLngBounds,
+ layerGroup,
+ Polyline,
+ Polygon,
+ LeafletMap,
+ LeafletMouseEvent,
+ LeafletEvent,
+ polygon,
+ polyline,
+ ControlOptions,
+ LayerGroup,
+ LatLng,
+ Point,
+ Layer,
+} from "@carma/leaflet";
+import * as L from "leaflet";
+import "leaflet-draw";
+import "@carma/types";
+import {
+ calculateArea,
+ calculateDistance,
+ formatDistance,
+ updateDistance,
+ updateDistanceByLatLngs,
+} from "../utils/measurement-geometry";
+import { createVertexClickHandler } from "../utils/vertex-click-handler";
+import { TOOLTIP_LABELS } from "../labels";
+import {
+ distanceBetweenLatLng,
+ EXACT_MATCH_METERS,
+ isFirstVertexMatch,
+} from "../utils/snapping";
+import { DRAWING_SHAPE_ID } from "../utils/constants";
+import {
+ MeasurementPolyline,
+ MeasurementPolygon,
+ MeasurementLayer,
+ MeasurementLeafletEvent,
+ MeasurementShapeData,
+} from "../types";
+
+// --- Type Definitions ---
+
+export interface MeasurementMarker extends L.Marker {
+ customHandle?: number;
+}
+
+export interface DrawHandler {
+ _poly?: { _latlngs: LatLng[] };
+ _enabled?: boolean;
+ enable(): void;
+ disable(): void;
+ completeShape?: () => void;
+ addVertex?(latlng: LatLng): void;
+ _markers?: MeasurementMarker[];
+}
+
+export interface MeasureControlOptions extends ControlOptions {
+ icon_lineActive: string;
+ icon_lineInactive: string;
+ icon_polygonActive: string;
+ icon_polygonInactive: string;
+ html_template: string;
+ height: number;
+ width: number;
+ mode_btn: string;
+ isDrawing: boolean;
+ changeModeButtonActive: boolean;
+ msj_disable_tool: string;
+ shapes: MeasurementShapeData[];
+ activeShape: number | string | symbol | null;
+ shapeMode: "line" | "polygon";
+ measurementOrder: number;
+ moveToShape: boolean | MeasurementShapeData | null;
+ cb: () => void;
+ cbSaveShape: (shape: MeasurementShapeData) => void;
+ cbDeleteShape: (
+ id: number | string | symbol,
+ localShapeStore: MeasurementShapeData[]
+ ) => void;
+ cbUpdateShape: (
+ id: number | string | symbol,
+ newCoordinates: number[][],
+ newDistance: string,
+ newSquare: string | null
+ ) => void;
+ cbVisiblePolylinesChange: (ids: (number | string | symbol)[]) => void;
+ cbSetDrawingStatus: (status: boolean) => void;
+ cbSetDrawingShape: (shape: MeasurementShapeData | null) => void;
+ cbSetActiveShape: (id: number | string | symbol) => void;
+ cbSetUpdateStatusHandler: (status: boolean) => void;
+ cbMapMovingEndHandler: (status: boolean) => void;
+ cbSaveLastActiveShapeIdBeforeDrawingHandler: () => void;
+ cbChangeActiveCancelledShapeId: () => void;
+ cbToggleMeasurementMode: () => void;
+ cbGetMeasurementModeHandler: () => void;
+ cbDeleteVisibleShapeById: (id: number | string | symbol) => void;
+ cbUpdateAreaOfDrawingMeasurement: (area: string | null) => void;
+ cbSetCurrentDrawHandler: (handler: DrawHandler | null) => void;
+ cbSetMapStatus?: (status: string) => void;
+ visiblePolylines: (string | number | symbol)[];
+ localShapeStore: MeasurementShapeData[];
+ isDrawingEmpty: boolean;
+ nativeMove: boolean;
+ currentLine: DrawHandler | null;
+ polygonMode: boolean;
+ enabled: boolean;
+ startDrawing: boolean;
+ customTooltip: HTMLElement | null;
+ device: "desktop" | "mobile" | "tablet" | "Desktop" | null;
+ clickAfterShapeSelection: boolean;
+ snappingLatlng: LatLng | null;
+ snappingEnabled: boolean;
+ snappingQueryRadius?: number;
+ measurementMode?: "measurement" | "other_mode"; // TODO: check what other modes exist or if this is correct type
+}
+
+export interface MeasureControl extends Control {
+ options: MeasureControlOptions;
+ _map: LeafletMap;
+ _measureLayers: LayerGroup;
+ _measureHandler: any;
+ _lastOriginalClick: { latlng: LatLng; containerPoint: Point };
+
+ _mapClickHandler?: (event: LeafletMouseEvent) => void;
+ _drawCreatedHandler?: (event: any) => void;
+ _drawDrawstartHandler?: (event: any) => void;
+ _drawDrawvertexHandler?: (event: any) => void;
+ _drawCanceledHandler?: () => void;
+ _moveendHandler?: (event: any) => void;
+ _mousemoveHandler?: (event: LeafletMouseEvent) => void;
+ _mouseoutHandler?: (event: LeafletMouseEvent) => void;
+ _vertexClickHandler?: (event: LeafletMouseEvent) => void;
+ _isFinishingShape?: boolean;
+ drawingLines(map: LeafletMap, event: LeafletMouseEvent): void;
+
+ onAdd(map: LeafletMap): HTMLElement;
+ _clearMeasurements(): void;
+ changeColorByActivePolyline(
+ map: LeafletMap,
+ customID: number | string | symbol
+ ): void;
+ changeColorByLastShape(map: LeafletMap): void;
+ showLastPolylineOnFirstLoding(map: LeafletMap): void;
+ getVisiblePolylines(map: LeafletMap): MeasurementPolyline[];
+ getVisiblePolylinesIds(polylines: MeasurementPolyline[]): void;
+ getAllPolylines(map: LeafletMap): MeasurementPolyline[];
+ removePolylineById(map: LeafletMap, customID: number | string | symbol): void;
+ fitMapToAllPolylines(map: LeafletMap): void;
+ fitMapToPolylines(map: LeafletMap, polylines: MeasurementPolyline[]): void;
+ convertPolylineToPolygon(map: LeafletMap, layer: MeasurementPolyline): void;
+ loadMeasurements(map?: LeafletMap): void;
+ _toggleMeasurementBtn(): void;
+ toggleMeasurementMode(ifChangeMode?: boolean, map?: LeafletMap): void;
+ _UpdateDistance(layer: MeasurementPolyline): string;
+ _toggleMeasure(id: string, iconActive: string, inactiveIcon: string): void;
+ calculateArea(coordinates: number[][]): string;
+ calculateDistance(latlngs: LatLng[]): number;
+ formatDistance(distance: number): string;
+ saveShapeHandler(
+ layer: MeasurementPolyline,
+ distance: string | null,
+ area: string | null,
+ map: LeafletMap
+ ): void;
+ _onPolylineDrag(event: LeafletEvent): void;
+ replaceLineToPolygon(
+ map: LeafletMap,
+ layer: MeasurementPolyline
+ ): MeasurementShapeData;
+ getVisibleShapeIdsArr(map: LeafletMap): (number | string | symbol)[];
+ _UpdateDistanceByLatLngs(coordinates: number[][]): string;
+ showActiveShape(map: LeafletMap, coordinates: number[][]): void;
+ setMeasurementEnabled(enabled: boolean, map: LeafletMap): void;
+ changeMeasurementsArr(arr: MeasurementShapeData[]): void;
+ findLastCreatedLayer(layerGroup: LayerGroup): Layer | null;
+ cancelDrawing(): void;
+ startDrawing(): void;
+ _onPolygonClick(map: LeafletMap, event: LeafletMouseEvent): void;
+ _UpdateAreaperimeter(layer: MeasurementPolygon): void;
+}
+
+// Placeholder for icons to not show broken images
+// Transparent 1x1 GIF (43 bytes)
+// See http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever
+const TRANSPARENT_PIXEL =
+ "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
+
+export const MeasureControl = Control.extend({
+ options: {
+ position: "topright",
+ icon_lineActive: TRANSPARENT_PIXEL,
+ icon_lineInactive: TRANSPARENT_PIXEL,
+ icon_polygonActive: TRANSPARENT_PIXEL,
+ icon_polygonInactive: TRANSPARENT_PIXEL,
+ html_template: `${TOOLTIP_LABELS.general.results}
+${TOOLTIP_LABELS.general.area}:
_p_area
+${TOOLTIP_LABELS.general.perimeter} :
_p_perimeter
`,
+ height: 130,
+ width: 150,
+ mode_btn: "",
+ isDrawing: false,
+ changeModeButtonActive: false,
+ msj_disable_tool: TOOLTIP_LABELS.general.disableTool,
+ shapes: [],
+ activeShape: null,
+ shapeMode: "line",
+ measurementOrder: 0,
+ moveToShape: false,
+ cb: function (...args: any[]) {},
+ cbSaveShape: function (...args: any[]) {},
+ cbDeleteShape: function (...args: any[]) {},
+ cbUpdateShape: function (...args: any[]) {},
+ cbVisiblePolylinesChange: function (...args: any[]) {},
+ cbSetDrawingStatus: function (...args: any[]) {},
+ cbSetDrawingShape: function (...args: any[]) {},
+ cbSetActiveShape: function (...args: any[]) {},
+ cbSetUpdateStatusHandler: function (...args: any[]) {},
+ cbMapMovingEndHandler: function (...args: any[]) {},
+ cbSaveLastActiveShapeIdBeforeDrawingHandler: function (...args: any[]) {},
+ cbChangeActiveCancelledShapeId: function (...args: any[]) {},
+ cbToggleMeasurementMode: function (...args: any[]) {},
+ cbGetMeasurementModeHandler: function (...args: any[]) {},
+ cbDeleteVisibleShapeById: function (...args: any[]) {},
+ cbUpdateAreaOfDrawingMeasurement: function (...args: any[]) {},
+ cbSetCurrentDrawHandler: function (...args: any[]) {},
+ cbSetMapStatus: function (...args: any[]) {},
+ visiblePolylines: [],
+ localShapeStore: [],
+ isDrawingEmpty: true,
+ nativeMove: false,
+ currentLine: null,
+ polygonMode: false,
+ enabled: false,
+ startDrawing: false,
+ customTooltip: null,
+ device: null,
+ clickAfterShapeSelection: false,
+ snappingLatlng: null,
+ snappingEnabled: true,
+ },
+
+ drawingLines: function (
+ this: MeasureControl,
+ map: LeafletMap,
+ event: LeafletMouseEvent
+ ) {
+ if (this.options.customTooltip) {
+ this.options.customTooltip.style.visibility = "hidden";
+ }
+ this.options.shapeMode = "line";
+ this._measureHandler = new L.Draw.Polyline(map as any, {
+ showLength: true,
+ shapeOptions: {
+ weight: 3,
+ color: "#267bdcd4",
+ opacity: 1,
+ },
+ });
+
+ const self = this;
+
+ // Override _updateGuide to snap the preview line
+ const originalUpdateGuide = (this._measureHandler as any)._updateGuide;
+ if (originalUpdateGuide) {
+ (this._measureHandler as any)._updateGuide = function (point: any) {
+ if (self.options.snappingLatlng) {
+ // If we have a snapping point, use it for the guide line
+ // We need to convert the latlng to a layer point, as that's what _updateGuide expects
+ const snappedPoint = map.latLngToLayerPoint(
+ self.options.snappingLatlng
+ );
+ return originalUpdateGuide.call(this, snappedPoint);
+ }
+ return originalUpdateGuide.call(this, point);
+ };
+ }
+
+ // DIAGNOSTIC: Hook into Leaflet.Draw's internal completion to see what triggers it
+ const originalFinishShape = (this._measureHandler as any)._finishShape;
+ if (originalFinishShape) {
+ (this._measureHandler as any)._finishShape = function (e) {
+ const eventInfo = e
+ ? {
+ type: e.type,
+ originalType: e.originalEvent?.type,
+ pointerType: e.originalEvent?.pointerType,
+ target: e.target?.className,
+ timeStamp: e.timeStamp,
+ }
+ : "no event";
+
+ // CRITICAL: Block Leaflet.Draw's native touch finishing
+ // We handle this manually in vertex-click-handler.ts to ensure consistent behavior
+ if (
+ e &&
+ (e.type?.startsWith("touch") ||
+ e.originalEvent?.type?.startsWith("touch") ||
+ e.originalEvent?.pointerType === "touch")
+ ) {
+ console.warn(
+ "[measure-path] Blocking native _finishShape on touch event",
+ eventInfo
+ );
+ return;
+ }
+
+ return originalFinishShape.apply(this, arguments);
+ };
+ }
+
+ const originalCompleteShape = (this._measureHandler as any).completeShape;
+ if (originalCompleteShape) {
+ (this._measureHandler as any).completeShape = function () {
+ console.warn("[measure-path] 🔴 completeShape() called!", {
+ stack: new Error().stack,
+ vertexCount: this._markers?.length || 0,
+ timestamp: Date.now(),
+ });
+ return originalCompleteShape.apply(this, arguments);
+ };
+ }
+
+ const originalAddVertex = (this._measureHandler as any).addVertex;
+ if (originalAddVertex) {
+ (this._measureHandler as any).addVertex = function (latlng) {
+ // Use snapped position if available
+ const finalLatlng =
+ self.options.snappingEnabled && self.options.snappingLatlng
+ ? self.options.snappingLatlng
+ : latlng;
+
+ console.debug("[measure-path] addVertex() called", {
+ original: latlng,
+ snapped: self.options.snappingLatlng,
+ final: finalLatlng,
+ currentVertexCount: this._markers?.length || 0,
+ timestamp: Date.now(),
+ });
+
+ // Check for duplicate vertex to prevent 0-length segments
+ if (this._markers && this._markers.length > 0) {
+ // Check for closing polygon (snapping to first vertex)
+ // We use isFirstVertexMatch (requires 3+ vertices) to detect closure
+ if (isFirstVertexMatch(this, finalLatlng, EXACT_MATCH_METERS)) {
+ console.debug(
+ "[measure-path] Closing polygon via addVertex override (snapped to start)"
+ );
+ // Explicitly set polygon mode
+ self.options.shapeMode = "polygon";
+
+ // Set finishing flag to prevent map click from starting new shape
+ (self as any)._isFinishingShape = true;
+
+ this._finishShape();
+ return;
+ }
+
+ const lastMarker = this._markers[this._markers.length - 1];
+ if (
+ distanceBetweenLatLng(finalLatlng, lastMarker.getLatLng()) <
+ EXACT_MATCH_METERS
+ ) {
+ console.warn(
+ "[measure-path] Preventing 0-length segment in addVertex - duplicate vertex ignored"
+ );
+ return;
+ }
+ }
+
+ (self as any)._lastVertexAdded = Date.now();
+ return originalAddVertex.call(this, finalLatlng);
+ };
+ }
+
+ this.options.currentLine = this._measureHandler;
+ this.options.cbSetCurrentDrawHandler(this._measureHandler);
+
+ const tooltipContent = `${TOOLTIP_LABELS.measurement.finishLine}
${TOOLTIP_LABELS.measurement.finishPolygon}`;
+
+ L.drawLocal.draw.handlers.polyline.tooltip.start = `${TOOLTIP_LABELS.measurement.start}
${TOOLTIP_LABELS.snapping.active}`;
+ L.drawLocal.draw.handlers.polyline.tooltip.cont = `${TOOLTIP_LABELS.measurement.continue}
${TOOLTIP_LABELS.snapping.active}`;
+ L.drawLocal.draw.handlers.polyline.tooltip.end = tooltipContent;
+
+ this._measureHandler.enable();
+
+ // DIAGNOSTIC: Intercept Leaflet.Draw's built-in dblclick handler
+ const originalDblClick = (this._measureHandler as any)._onMouseDblClick;
+ if (originalDblClick) {
+ console.warn(
+ "[measure-path] Found Leaflet.Draw dblclick handler - intercepting"
+ );
+ (this._measureHandler as any)._onMouseDblClick = function (e) {
+ console.error("[measure-path] LEAFLET.DRAW DBLCLICK HANDLER FIRED", {
+ eventType: e?.type,
+ originalEvent: e?.originalEvent?.type,
+ pointerType: e?.originalEvent?.pointerType,
+ timeStamp: e?.timeStamp,
+ vertexCount: this._markers?.length || 0,
+ timestamp: Date.now(),
+ stack: new Error().stack,
+ });
+ return originalDblClick.apply(this, arguments);
+ };
+ } else {
+ console.log(
+ "[measure-path] No dblclick handler found on _measureHandler"
+ );
+ }
+
+ // DIAGNOSTIC: Log all active event listeners on the map
+ console.log("[measure-path] Active map event listeners:", {
+ hasClick: map.listens("click"),
+ hasDblClick: map.listens("dblclick"),
+ clickCount: map.listens("click", true),
+ dblclickCount: map.listens("dblclick", true),
+ timestamp: Date.now(),
+ });
+
+ const latlng =
+ this.options.snappingEnabled && this.options.snappingLatlng
+ ? this.options.snappingLatlng
+ : event.latlng;
+
+ // CRITICAL: Validate coordinates before adding vertex
+ // During map transitions, coordinates can become NaN
+ if (!latlng || isNaN(latlng.lat) || isNaN(latlng.lng)) {
+ console.warn(
+ "[measure-path] BLOCKING addVertex with invalid coordinates:",
+ latlng
+ );
+ return; // Don't add invalid vertex
+ }
+
+ this.options.currentLine.addVertex(latlng);
+
+ const tooltip = document.querySelector(
+ ".leaflet-draw-tooltip"
+ ) as HTMLElement;
+
+ const pos = map.latLngToLayerPoint(latlng);
+ DomUtil.setPosition(tooltip, pos);
+
+ this._toggleMeasure(
+ "img_plg_lines",
+ "icon_lineActive",
+ "icon_lineInactive"
+ );
+ },
+
+ startDrawing: function (this: MeasureControl) {
+ this.options.startDrawing = true;
+ },
+
+ saveShapeHandler: function (
+ this: MeasureControl,
+ layer: MeasurementPolyline,
+ distance: string | null = null,
+ area: string | null = null,
+ map: LeafletMap
+ ) {
+ const latlngs = layer.getLatLngs();
+ const latlngsJSON = layer.toGeoJSON();
+ const shapeId = layer._leaflet_id;
+ layer.customID = shapeId;
+ console.log("[measure-path] layer click handler added", shapeId);
+ layer.on("click", (e) => {
+ // If we are snapped and in measurement mode (and not currently drawing),
+ // we want to start a new measurement snapped to this point, NOT select the shape.
+ // Leaflet event propagation will handle the map click to start drawing.
+ if (
+ this.options.snappingLatlng &&
+ this.options.measurementMode === "measurement" &&
+ !this.options.isDrawing
+ ) {
+ console.debug(
+ "[measure-path] Click on shape ignored (snapping active) - letting map click handle it"
+ );
+ return;
+ }
+
+ this.options.cbSetActiveShape(layer.customID);
+ this.options.cbSetUpdateStatusHandler(false);
+ });
+
+ if (this.options.shapeMode === "polygon") {
+ const polygon = this.replaceLineToPolygon(map, layer);
+ this.options.cbSaveShape(polygon);
+ this.getVisibleShapeIdsArr(map);
+ } else {
+ const prepareCoordinates =
+ this.options.shapeMode === "line"
+ ? latlngsJSON.geometry.coordinates
+ : latlngsJSON.geometry.coordinates[0];
+ const reversedCoordinates = prepareCoordinates.map((item) => {
+ return item.reverse();
+ });
+
+ const preparePolygon = {
+ coordinates: reversedCoordinates,
+ options: {
+ color: "#267bdcd4",
+ fillColor: null,
+ opacity: 0.5,
+ weight: 4,
+ },
+ shapeId,
+ distance,
+ number: this.options.measurementOrder,
+ area,
+ shapeType: this.options.shapeMode,
+ };
+ this.options.cbSaveShape(preparePolygon);
+ this.getVisibleShapeIdsArr(map);
+ }
+ },
+
+ _onPolylineDrag: function (this: MeasureControl, event: LeafletEvent) {
+ if (this.options.customTooltip) {
+ this.options.customTooltip.style.visibility = "hidden";
+ }
+ this.options.cbSetUpdateStatusHandler(true);
+
+ // Set status based on drag type
+ if (this.options.cbSetMapStatus) {
+ if (
+ event.type === "editable:drag" ||
+ event.type === "editable:dragstart"
+ ) {
+ // Dragging whole shape
+ this.options.cbSetMapStatus("MOVING");
+ } else if (event.type === "editable:vertex:drag") {
+ // Dragging vertices or deleting vertex
+ this.options.cbSetMapStatus("EDITING");
+ }
+ }
+
+ const polyline = event.target;
+ const layer = event.layer;
+ this.options.cbSetActiveShape(layer.customID);
+ const latlngsJSON = layer.toGeoJSON();
+ const isLine = layer.toGeoJSON().geometry.type === "LineString";
+ const prepareCoordinates = isLine
+ ? latlngsJSON.geometry.coordinates
+ : latlngsJSON.geometry.coordinates[0];
+ const reversedCoordinates = prepareCoordinates.map((item) => {
+ return item.reverse();
+ });
+
+ const square = !isLine ? calculateArea(reversedCoordinates) : null;
+ polyline.updateMeasurements();
+ const newDistance = updateDistance(layer);
+ const shapeId = polyline?.customID
+ ? polyline?.customID
+ : polyline._leaflet_id;
+
+ this.options.cbUpdateShape(
+ shapeId,
+ reversedCoordinates,
+ newDistance,
+ square
+ );
+ this.options.isDrawing = false;
+ },
+
+ _onPolygonClick: function (
+ this: MeasureControl,
+ map: LeafletMap,
+ event: LeafletMouseEvent
+ ) {
+ const clickedPolygon = event.target;
+ const latlngs = clickedPolygon.getLatLngs();
+
+ this._measureLayers.removeLayer(clickedPolygon._leaflet_id);
+ const shapeId = clickedPolygon?.customID
+ ? clickedPolygon?.customID
+ : clickedPolygon._leaflet_id;
+
+ this.options.cbDeleteShape(shapeId, this.options.localShapeStore);
+
+ const allPolyLines = this.getVisiblePolylines(map);
+ this.getVisiblePolylinesIds(allPolyLines);
+ },
+
+ onAdd: function (this: MeasureControl, map: LeafletMap) {
+ const linesContainer = DomUtil.create(
+ "div",
+ "leaflet-bar leaflet-control dont-show m-container"
+ );
+ const lineIcon = DomUtil.create("a", "", linesContainer);
+ lineIcon.innerHTML = `
+
+

+
+ `;
+ lineIcon.href = "#";
+ lineIcon.title = TOOLTIP_LABELS.general.measurementMode;
+
+ const iconsWrapper = DomUtil.create("div", "m-icons-wrapper");
+ iconsWrapper.appendChild(linesContainer);
+
+ console.log("[measure-path] icon click handler added");
+
+ DomEvent.on(
+ lineIcon,
+ "click",
+ (event) => {
+ event.preventDefault(); // Prevent default action (e.g., redirection)
+ this.toggleMeasurementMode();
+ },
+ this
+ );
+
+ this._map = map;
+
+ this._measureLayers = layerGroup().addTo(map);
+
+ console.log(
+ "[measure-path] map click handler added",
+ (this._map as unknown as { _leaflet_id: number })._leaflet_id
+ );
+
+ // Store handler references for proper cleanup
+ this._mapClickHandler = (event) => {
+ const enabled = this.options.enabled;
+
+ console.log("[measure-path] Map clicked", this.options, {
+ isDrawing: this.options.isDrawing,
+ enabled,
+ clickAfterShapeSelection: this.options.clickAfterShapeSelection,
+ isFinishingShape: (this as any)._isFinishingShape,
+ eventType: event.originalEvent?.type,
+ isSyntheticSnap: !!(event as any)._isSyntheticSnap,
+ targetClassName: (event.originalEvent?.target as HTMLElement)
+ ?.className,
+ latlng: event.latlng,
+ timestamp: Date.now(),
+ });
+
+ // Don't start new measurement if we're finishing one
+ if ((this as any)._isFinishingShape) {
+ console.log(
+ "[measure-path] Ignoring map click - currently finishing shape"
+ );
+ // Clear flag immediately so next click works
+ (this as any)._isFinishingShape = false;
+ return;
+ }
+
+ if (!this.options.isDrawing && enabled) {
+ this.drawingLines(map, event);
+ this.options.isDrawing = true;
+ } else {
+ // this.options.isDrawing = false;
+ }
+
+ if (this.options.clickAfterShapeSelection) {
+ this.options.isDrawing = false;
+ this.options.clickAfterShapeSelection = false;
+ }
+ };
+
+ this._drawCreatedHandler = (event) => {
+ console.log("[measure-path] ========== draw:created FIRED ==========", {
+ layerType: event.layerType,
+ vertexCount: event.layer.getLatLngs?.()?.length || 0,
+ timestamp: Date.now(),
+ });
+
+ // Reset finishing flag since the shape is successfully created
+ // CRITICAL: Use setTimeout to ensure _mapClickHandler sees this as TRUE for the current event loop
+ // If we reset it synchronously, _mapClickHandler (which runs after this) will think we are done and start a new shape
+ setTimeout(() => {
+ (this as any)._isFinishingShape = false;
+ }, 0);
+
+ this.options.isDrawing = false;
+ this.options.isDrawingEmpty = true;
+
+ this.options.cbSetDrawingStatus(false);
+ this.options.cbSetDrawingShape(null);
+
+ // Re-enable edit on existing shapes
+ this._measureLayers.eachLayer((layer: any) => {
+ if (layer.enableEdit) {
+ layer.enableEdit();
+ }
+ });
+
+ const layer = event.layer;
+ // layer.on("dblclick", this._onPolygonClick.bind(this, map));
+
+ layer.on("editable:vertex:dragend", () => {
+ this.options.cbSetUpdateStatusHandler(false);
+ // Reset status to WAITING when vertex editing ends
+ if (this.options.cbSetMapStatus) {
+ this.options.cbSetMapStatus("WAITING");
+ }
+ });
+
+ // Reset status to WAITING when drag ends
+ layer.on("editable:dragend", () => {
+ if (this.options.cbSetMapStatus) {
+ this.options.cbSetMapStatus("WAITING");
+ }
+ });
+
+ // Add style to polygon
+ layer.addTo(this._measureLayers).showMeasurements().enableEdit();
+ layer.options.draggable = false;
+
+ const distance = updateDistance(layer);
+
+ this.saveShapeHandler(layer, distance, null, map);
+
+ layer.on(
+ "editable:drag editable:vertex:drag editable:vertex:deleted editable:dragstart editable:dragend",
+ this._onPolylineDrag.bind(this)
+ );
+
+ this.options.isDrawing = false;
+
+ this._measureHandler.disable();
+ };
+
+ this._drawDrawstartHandler = (event) => {
+ console.warn(
+ "[measure-path] ========== draw:drawstart FIRED ==========",
+ {
+ layerType: event.layerType,
+ timestamp: Date.now(),
+ }
+ );
+
+ // Disable edit on existing shapes to remove grab cursors and prevent interaction conflicts
+ this._measureLayers.eachLayer((layer: any) => {
+ if (layer.disableEdit) {
+ layer.disableEdit();
+ }
+ });
+
+ const mouseActive = Browser.touch && matchMedia("(hover:hover)").matches;
+ if (
+ mouseActive ||
+ event.layerType === "circle" ||
+ event.layerType === "rectangle"
+ ) {
+ event.target.touchExtend.enable();
+ } else {
+ event.target.touchExtend.disable();
+ }
+ this.options.cbSaveLastActiveShapeIdBeforeDrawingHandler();
+ this.options.measurementOrder = this.options.measurementOrder + 1;
+ this.changeColorByActivePolyline(map, "ddfsc1231");
+ };
+
+ // Create vertex click handler once and attach to map (event delegation)
+ // This handler listens to ALL map clicks but only processes clicks on vertex markers
+ if (!this._vertexClickHandler) {
+ // Initialize flag to track when we're finishing a shape
+ (this as any)._isFinishingShape = false;
+
+ this._vertexClickHandler = createVertexClickHandler(
+ () => this._measureHandler,
+ this.options as any,
+ () => this._measureHandler?._markers?.length || 0,
+ map,
+ () => (this as any)._isFinishingShape,
+ (value: boolean) => {
+ (this as any)._isFinishingShape = value;
+ },
+ () => (this as any)._lastVertexAdded || 0
+ );
+ // Handler is now attached to individual markers in _drawDrawvertexHandler
+ // to ensure we can stop propagation before it reaches the map
+ // map.on("click", this._vertexClickHandler);
+ console.log(
+ "[measure-path] Created vertex handler (will attach to markers)"
+ );
+ }
+
+ this._drawDrawvertexHandler = (event) => {
+ const layers = event.layers;
+ const latlngs = [];
+ let index = 0;
+ let firsHovering = false;
+
+ layers.eachLayer((layer) => {
+ const markerLatLng = layer.getLatLng();
+ layer.customHandle = index++;
+
+ // Leaflet.Draw attaches _finishShape click handlers DURING this event
+ // We need to remove them AFTER the event completes
+ setTimeout(() => {
+ const layerEvents = (layer as any)._events;
+ if (layerEvents) {
+ console.log("[measure-path] Marker listeners before cleanup:", {
+ handle: layer.customHandle,
+ hasClick: !!layerEvents.click,
+ clickCount: layerEvents.click?.length || 0,
+ hasTouchend: !!layerEvents.touchend,
+ touchendCount: layerEvents.touchend?.length || 0,
+ });
+
+ // Remove Leaflet.Draw's _finishShape handler
+ // Source: leaflet.draw-src.js line 856, 1129
+ layer.off(
+ "click",
+ (this._measureHandler as any)._finishShape,
+ this._measureHandler
+ );
+ layer.off(
+ "dblclick",
+ (this._measureHandler as any)._finishShape,
+ this._measureHandler
+ );
+
+ // Attach our custom handler to the marker
+ // This handler MUST stop propagation to prevent the map from seeing the click
+ layer.on("click", this._vertexClickHandler);
+
+ // CRITICAL: Stop native touchstart propagation on the DOM element
+ // Leaflet.Draw listens to 'touchstart' on the map container to add vertices.
+ // We must stop the event at the marker icon DOM level to prevent it from bubbling to the map.
+ // layer.on('touchstart') is too late because it's a Leaflet event, not a native DOM capture.
+ const icon = layer.getElement();
+ if (icon) {
+ L.DomEvent.on(icon, "touchstart", L.DomEvent.stopPropagation);
+ L.DomEvent.on(icon, "touchend", L.DomEvent.stopPropagation);
+ L.DomEvent.on(icon, "touchmove", L.DomEvent.stopPropagation);
+ console.log(
+ "[measure-path] Added native touch blockers to marker icon"
+ );
+ }
+
+ // Also stop Leaflet-level touchstart just in case
+ layer.on("touchstart", (e) => {
+ if (e.originalEvent) {
+ L.DomEvent.stopPropagation(e.originalEvent);
+ }
+ });
+
+ // We don't want to remove 'touchstart' listener we just added, so only remove touchend
+ layer.off("touchend");
+ // layer.off("touchstart"); // Don't remove this, we just added our own!
+
+ console.log("[measure-path] Removed Leaflet.Draw marker listeners");
+ }
+ }, 0);
+
+ layer.on("mouseover", (e) => {
+ const coordinates = (this._measureHandler as L.Control.DrawHandler)
+ ._poly._latlngs;
+ const latLngArray = coordinates.map((c) => [c.lat, c.lng]);
+ latLngArray.push(latLngArray[0]);
+ const area = calculateArea(latLngArray);
+ if (e.target.customHandle === 0 && firsHovering) {
+ this.options.cbUpdateAreaOfDrawingMeasurement(area);
+ L.drawLocal.draw.handlers.polyline.tooltip.end =
+ TOOLTIP_LABELS.measurement.finishPolygon;
+ }
+ firsHovering = true;
+ });
+
+ layer.on("mouseout", (e) => {
+ if (e.target.customHandle === 0) {
+ const tooltipContent = `${TOOLTIP_LABELS.measurement.finishLine}
${TOOLTIP_LABELS.measurement.finishPolygon}`;
+ L.drawLocal.draw.handlers.polyline.tooltip.end = tooltipContent;
+ this.options.cbUpdateAreaOfDrawingMeasurement(null);
+ }
+ });
+
+ const latLng = layer.getLatLng();
+ latlngs.push(latLng);
+
+ // Add mouseover/mouseout for last vertex to show finish tooltip
+ // Only when hovering over the last marker, not unconditionally
+ layer.on("mouseover", (e) => {
+ const isLastVertex = e.target.customHandle === index;
+ if (isLastVertex && index >= 1) {
+ if (index >= 2) {
+ L.drawLocal.draw.handlers.polyline.tooltip.end =
+ TOOLTIP_LABELS.measurement.finishPolygonHover;
+ } else {
+ L.drawLocal.draw.handlers.polyline.tooltip.end =
+ TOOLTIP_LABELS.measurement.finishLineHover;
+ }
+ }
+ });
+
+ layer.on("mouseout", (e) => {
+ const isLastVertex = e.target.customHandle === index;
+ if (isLastVertex && index >= 1) {
+ // Reset to default tooltip
+ L.drawLocal.draw.handlers.polyline.tooltip.end = `${TOOLTIP_LABELS.measurement.finishLine}
${TOOLTIP_LABELS.measurement.finishPolygon}`;
+ }
+ });
+ });
+
+ const formatPerimeter = calculateDistance(latlngs);
+ const distance = formatDistance(formatPerimeter);
+
+ if (this.options.isDrawingEmpty) {
+ const shapesObj = {
+ coordinates: [latlngs],
+ distance,
+ shapeId: DRAWING_SHAPE_ID,
+ number: this.options.measurementOrder,
+ shapeType: "line" as const,
+ options: {
+ color: "#267bdcd4",
+ fillColor: null,
+ opacity: 0.5,
+ weight: 3,
+ },
+ };
+
+ this.options.isDrawingEmpty = false;
+ this.options.cbSetDrawingStatus(true);
+ // this.options.cbSaveShape(shapesObj);
+ this.options.cbSetDrawingShape(shapesObj);
+ } else {
+ const shapesObj = {
+ coordinates: [latlngs],
+ distance,
+ shapeId: DRAWING_SHAPE_ID,
+ shapeType: "line" as const,
+ number: this.options.measurementOrder,
+ options: {
+ color: "#267bdcd4",
+ fillColor: null,
+ opacity: 0.5,
+ weight: 3,
+ },
+ };
+ this.options.cbSetDrawingShape(shapesObj);
+ }
+ };
+
+ this._drawCanceledHandler = () => {
+ this.options.isDrawing = true;
+ this.options.cbSetDrawingStatus(false);
+
+ this._measureHandler.disable();
+
+ // Re-enable edit on existing shapes
+ this._measureLayers.eachLayer((layer: any) => {
+ if (layer.enableEdit) {
+ layer.enableEdit();
+ }
+ });
+
+ this._toggleMeasure(
+ "img_plg_lines",
+ "icon_lineActive",
+ "icon_lineInactive"
+ );
+
+ this.options.cbDeleteVisibleShapeById(DRAWING_SHAPE_ID);
+ this.options.cbChangeActiveCancelledShapeId();
+ };
+
+ this._moveendHandler = (event) => {
+ const allPolyLines = this.getVisiblePolylines(map);
+ this.getVisiblePolylinesIds(allPolyLines);
+ this.options.cbMapMovingEndHandler(true);
+ this.options.cbSetUpdateStatusHandler(false);
+ };
+
+ this._mousemoveHandler = (event) => {
+ const target = event.originalEvent.target;
+ const isDesktop = this.options.device === "Desktop" ? true : false;
+ const enabled = this.options.enabled;
+ // this._propagateEventToUnderlyingLayers(map, event, "mouseover");
+
+ if (isDesktop) {
+ if (!this.options.customTooltip && enabled) {
+ const popupPane = map._panes.popupPane;
+
+ this.options.customTooltip = DomUtil.create(
+ "div",
+ "leaflet-draw-custom-tooltip",
+ popupPane
+ );
+
+ this.options.customTooltip.innerHTML = `${TOOLTIP_LABELS.measurement.start}
`;
+ this.options.customTooltip.style.visibility = "inherit";
+
+ const pos = this._map.latLngToLayerPoint(event.latlng);
+ DomUtil.setPosition(this.options.customTooltip, pos);
+ }
+
+ if (this.options.customTooltip && enabled) {
+ const latlng =
+ this.options.snappingEnabled && this.options.snappingLatlng
+ ? this.options.snappingLatlng
+ : event.latlng;
+
+ const pos = this._map.latLngToLayerPoint(latlng);
+ // const offsetX = 20;
+ const offsetX = 0;
+ // DomUtil.setPosition(this.options.customTooltip, pos);
+ DomUtil.setPosition(
+ this.options.customTooltip,
+ point(pos.x + offsetX, pos.y)
+ );
+ if ((target as HTMLElement).classList.contains("leaflet-div-icon")) {
+ this.options.customTooltip.style.visibility = "hidden";
+ }
+ if (
+ ((target as HTMLElement).classList.contains("leaflet-container") ||
+ (target as HTMLElement).classList.contains("leaflet-gl-layer")) &&
+ this.options.isDrawingEmpty
+ ) {
+ this.options.customTooltip.style.visibility = "visible";
+ }
+ }
+ }
+ };
+
+ this._mouseoutHandler = (event) => {
+ if (this.options.customTooltip) {
+ this.options.customTooltip.style.visibility = "hidden";
+ }
+ };
+
+ map.on("click", this._mapClickHandler);
+ map.on("draw:created", this._drawCreatedHandler);
+ map.on("draw:drawstart", this._drawDrawstartHandler);
+ map.on("draw:drawvertex", this._drawDrawvertexHandler);
+ map.on("draw:canceled", this._drawCanceledHandler);
+ map.on("moveend", this._moveendHandler);
+ map.on("mousemove", this._mousemoveHandler);
+ map.on("mouseout", this._mouseoutHandler);
+
+ return iconsWrapper;
+ },
+
+ onRemove: function (this: MeasureControl, map: LeafletMap) {
+ // Clean up all event handlers to prevent memory leaks and duplicate handlers on HMR
+ console.log("[measure-path] onRemove: Cleaning up event handlers");
+
+ // Remove only OUR specific event handlers by reference
+ if (this._mapClickHandler) map.off("click", this._mapClickHandler);
+ if (this._drawCreatedHandler)
+ map.off("draw:created", this._drawCreatedHandler);
+ if (this._drawDrawstartHandler)
+ map.off("draw:drawstart", this._drawDrawstartHandler);
+ if (this._drawDrawvertexHandler)
+ map.off("draw:drawvertex", this._drawDrawvertexHandler);
+ if (this._drawCanceledHandler)
+ map.off("draw:canceled", this._drawCanceledHandler);
+ if (this._moveendHandler) map.off("moveend", this._moveendHandler);
+ if (this._mousemoveHandler) map.off("mousemove", this._mousemoveHandler);
+ if (this._mouseoutHandler) map.off("mouseout", this._mouseoutHandler);
+
+ // Vertex click handler is attached to markers, which are removed automatically
+ // if (this._vertexClickHandler) {
+ // map.off("click", this._vertexClickHandler);
+ // }
+
+ // Remove measure layers
+ if (this._measureLayers) {
+ this._measureLayers.clearLayers();
+ map.removeLayer(this._measureLayers);
+ }
+
+ // Disable active drawing handler
+ if (this._measureHandler) {
+ this._measureHandler.disable();
+ }
+ },
+
+ _UpdateAreaperimeter: function (this: MeasureControl, layer: any) {
+ const latlngs = layer.getLatLngs()[0];
+
+ const options = {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ };
+ },
+
+ _toggleMeasure: function (
+ this: MeasureControl,
+ btnId = "",
+ activeIcon = "",
+ inactiveIcon = ""
+ ) {
+ if (this.options.isDrawing) {
+ this.options.isDrawing = false;
+ } else {
+ this._measureHandler.enable();
+ }
+ },
+
+ _clearMeasurements: function (this: MeasureControl) {
+ this._measureLayers.clearLayers();
+ },
+
+ changeColorByActivePolyline: function (
+ this: MeasureControl,
+ map: LeafletMap,
+ customID: string
+ ) {
+ this._measureLayers.eachLayer(function (layer) {
+ const polyline = layer as unknown as MeasurementPolyline;
+ if (layer instanceof Polyline) {
+ if ((layer as unknown as MeasurementPolyline).customID === customID) {
+ (polyline as MeasurementPolyline)._path.classList.remove(
+ "custom-polyline"
+ );
+ (polyline as MeasurementPolyline).enableEdit();
+ } else {
+ (polyline as MeasurementPolyline)._path.classList.add(
+ "custom-polyline"
+ );
+ (polyline as MeasurementPolyline).disableEdit();
+ }
+ }
+ });
+ },
+
+ changeColorByLastShape: function (this: MeasureControl, map: LeafletMap) {
+ let lastPolyline = null;
+
+ this._measureLayers.eachLayer(function (layer) {
+ if (layer instanceof Polyline) {
+ lastPolyline = layer;
+ layer._path.classList.add("custom-polyline");
+ }
+ });
+
+ if (lastPolyline) {
+ lastPolyline._path.classList.remove("custom-polyline");
+ }
+ },
+
+ getVisiblePolylines: function (this: MeasureControl, map: LeafletMap) {
+ const visiblePolylines = [];
+ const mapBounds = map.getBounds();
+
+ this._measureLayers.eachLayer(function (layer) {
+ if (layer instanceof Polyline) {
+ if (mapBounds.intersects(layer.getBounds())) {
+ visiblePolylines.push(layer);
+ }
+ }
+ });
+
+ return visiblePolylines;
+ },
+
+ getVisiblePolylinesIds: function (this: MeasureControl, polylinesArr: any[]) {
+ const idsPolylinesArr = [];
+ this.options.visiblePolylines = [];
+ polylinesArr.forEach((m) => {
+ idsPolylinesArr.push(m.customID);
+ this.options.visiblePolylines.push(m.customID);
+ });
+
+ this.options.cbVisiblePolylinesChange(idsPolylinesArr);
+ },
+
+ getAllPolylines: function (this: MeasureControl, map: LeafletMap) {
+ const polylines = [];
+
+ this._measureLayers.eachLayer(function (layer) {
+ if (layer instanceof Polyline) {
+ polylines.push(layer);
+ }
+ });
+
+ return polylines;
+ },
+
+ removePolylineById: function (
+ this: MeasureControl,
+ map: LeafletMap,
+ customID: string
+ ) {
+ const self = this;
+ this._measureLayers.eachLayer(function (layer) {
+ if (layer instanceof Polyline && layer.customID === customID) {
+ self._measureLayers.removeLayer(layer);
+ }
+ });
+ },
+
+ showActiveShape: function (
+ this: MeasureControl,
+ map: LeafletMap,
+ coordinates: any
+ ) {
+ this.options.moveToShape = true;
+ const bounds = latLngBounds(coordinates as L.LatLngExpression[]);
+ map.fitBounds(bounds);
+ },
+
+ fitMapToPolylines: function (
+ this: MeasureControl,
+ map: LeafletMap,
+ polylines: any[]
+ ) {
+ if (polylines.length === 0) {
+ return;
+ }
+
+ const allBounds = latLngBounds(
+ polylines[0].getBounds().getNorthEast(),
+ polylines[0].getBounds().getSouthWest()
+ );
+
+ polylines.forEach((polyline) => {
+ const polylineBounds = polyline.getBounds();
+ allBounds.extend(polylineBounds);
+ });
+
+ map.fitBounds(allBounds);
+ },
+
+ replaceLineToPolygon: function (
+ this: MeasureControl,
+ map: LeafletMap,
+ layer: any
+ ) {
+ const latlngsJSON = layer.toGeoJSON();
+ const prepareCoordinates = latlngsJSON.geometry.coordinates.map((l) => {
+ return l.reverse();
+ });
+
+ map.removeLayer(layer);
+
+ prepareCoordinates.push(prepareCoordinates[0]);
+
+ const options = {
+ color: "#267bdcd4",
+ fillColor: "#267bdcd4",
+ opacity: 1,
+ weight: 3,
+ };
+ const distance = updateDistanceByLatLngs(prepareCoordinates);
+ const square = calculateArea(prepareCoordinates);
+ const preparePolygon = {
+ coordinates: prepareCoordinates,
+ options,
+ shapeId: layer.customID,
+ distance: distance,
+ number: this.options.measurementOrder,
+ area: square,
+ shapeType: this.options.shapeMode,
+ };
+
+ const polygonLayer = polygon(prepareCoordinates, options);
+
+ polygonLayer.customID = layer.customID;
+ polygonLayer.customShape = "polygon";
+
+ polygonLayer.addTo(this._measureLayers).showMeasurements().enableEdit();
+ // polygonLayer.on("dblclick", this._onPolygonClick.bind(this, map));
+ polygonLayer.on("click", () => {
+ this.options.cbSetActiveShape(polygonLayer.customID);
+ this.options.cbSetUpdateStatusHandler(false);
+ this.options.isDrawing = false;
+ });
+ polygonLayer.on(
+ "editable:drag editable:dragstart editable:dragend editable:vertex:drag editable:vertex:deleted",
+ this._onPolylineDrag.bind(this)
+ );
+
+ polygonLayer.on("editable:vertex:dragend", () => {
+ this.options.cbSetUpdateStatusHandler(false);
+ // Reset status to WAITING when vertex editing ends
+ if (this.options.cbSetMapStatus) {
+ this.options.cbSetMapStatus("WAITING");
+ }
+ });
+
+ // Reset status to WAITING when drag ends
+ polygonLayer.on("editable:dragend", () => {
+ if (this.options.cbSetMapStatus) {
+ this.options.cbSetMapStatus("WAITING");
+ }
+ });
+
+ this.options.polygonMode = false;
+
+ this.options.isDrawing = true;
+
+ this._toggleMeasure(
+ "img_plg_lines",
+ "icon_lineActive",
+ "icon_lineInactive"
+ );
+
+ // this.options.isDrawing = false;
+
+ // this._measureHandler.disable();
+
+ return preparePolygon;
+ },
+ getVisibleShapeIdsArr: function (this: MeasureControl, map: LeafletMap) {
+ const allPolyLines = this.getVisiblePolylines(map);
+ this.getVisiblePolylinesIds(allPolyLines);
+ return this.options.visiblePolylines;
+ },
+
+ findLastCreatedLayer: function (this: MeasureControl, layerGroup: any) {
+ let lastLayer = null;
+ let highestId = -1;
+
+ layerGroup.eachLayer((layer) => {
+ if (layer._leaflet_id > highestId) {
+ highestId = layer._leaflet_id;
+ lastLayer = layer;
+ }
+ });
+
+ return lastLayer;
+ },
+
+ loadMeasurements: function (this: MeasureControl, map?: LeafletMap) {
+ if (this.options.shapes.length !== 0) {
+ this.options.shapes.forEach((shape) => {
+ const { coordinates, options, shapeId, shapeType } = shape;
+
+ const savedShape =
+ shapeType === "line"
+ ? polyline(
+ coordinates as any,
+ {
+ showLength: true,
+ className: "custom-polyline",
+ shapeOptions: {
+ weight: 4,
+ color: "#267bdcd4",
+ opacity: 1,
+ },
+ } as any
+ )
+ : polygon(
+ coordinates as any,
+ {
+ showLength: true,
+ className: "custom-polyline",
+ shapeOptions: {
+ weight: 4,
+ color: "#267bdcd4",
+ opacity: 1,
+ },
+ } as any
+ );
+
+ (savedShape as any).customID = shapeId;
+ savedShape.addTo(this._measureLayers).showMeasurements().enableEdit();
+ savedShape.on("click", () => {
+ this.options.isDrawing = true;
+ this.options.cbSetActiveShape(savedShape.customID);
+ this.options.cbSetUpdateStatusHandler(false);
+ this.options.clickAfterShapeSelection = true;
+ });
+ savedShape.on("mouseout", (e) => {
+ // this.options.isDrawing = false;
+ });
+ savedShape.on("mouseover", (e) => {
+ if (this.options.customTooltip) {
+ this.options.customTooltip.style.visibility = "hidden";
+ }
+ });
+ savedShape.on(
+ "editable:drag editable:dragstart editable:dragend editable:vertex:drag editable:vertex:deleted",
+ this._onPolylineDrag.bind(this)
+ );
+
+ savedShape.on("editable:vertex:dragend", () => {
+ this.options.cbSetUpdateStatusHandler(false);
+ // Reset status to WAITING when vertex editing ends
+ if (this.options.cbSetMapStatus) {
+ this.options.cbSetMapStatus("WAITING");
+ }
+ });
+
+ // Reset status to WAITING when drag ends
+ savedShape.on("editable:dragend", () => {
+ if (this.options.cbSetMapStatus) {
+ this.options.cbSetMapStatus("WAITING");
+ }
+ });
+ });
+ }
+ },
+
+ _toggleMeasurementBtn: function (this: MeasureControl) {
+ if (this.options.changeModeButtonActive) {
+ (document.getElementById("img_plg_lines") as HTMLImageElement).src =
+ this.options.icon_lineInactive;
+ this.options.changeModeButtonActive = false;
+ } else {
+ (document.getElementById("img_plg_lines") as HTMLImageElement).src =
+ this.options.icon_lineActive;
+ this.options.changeModeButtonActive = true;
+ }
+ },
+
+ toggleMeasurementMode: function (
+ this: MeasureControl,
+ ifChangeMode = true,
+ map?: LeafletMap
+ ) {
+ const enabled = this.options.enabled;
+ if (enabled) {
+ L.drawLocal.draw.handlers.polyline.tooltip.start =
+ TOOLTIP_LABELS.measurement.start;
+ this._clearMeasurements();
+ this.loadMeasurements();
+ // const drawBtn = document.getElementById("draw_shape");
+ // drawBtn.classList.remove("hide-draw-btn");
+
+ (document.getElementById("img_plg_lines") as HTMLImageElement).src =
+ this.options.icon_lineActive;
+
+ // if (this.options.isFirstLoading) {
+ // this.options.isFirstLoading = false;
+ // }
+
+ const customTooltip = document.querySelector("#routedMap") as HTMLElement;
+ customTooltip.style.cursor = "crosshair";
+ } else {
+ this._clearMeasurements();
+
+ const customTooltip = document.querySelector("#routedMap") as HTMLElement;
+ customTooltip.style.cursor = "pointer";
+ if (this.options.currentLine) {
+ this.options.currentLine.disable();
+ }
+ customTooltip.style.cursor = "pointer";
+ // const drawBtn = document.getElementById("draw_shape");
+ // drawBtn.classList.add("hide-draw-btn");
+ this.options.isDrawing = false;
+ (document.getElementById("img_plg_lines") as HTMLImageElement).src =
+ this.options.icon_lineInactive;
+ }
+
+ if (ifChangeMode) {
+ this.options.cbToggleMeasurementMode();
+ }
+ },
+
+ setMeasurementEnabled: function (
+ this: MeasureControl,
+ enabled: boolean,
+ map: LeafletMap
+ ) {
+ this.options.enabled = enabled;
+ this.toggleMeasurementMode(false, map);
+ },
+ changeMeasurementsArr: function (this: MeasureControl, arr: any[]) {
+ this.options.shapes = arr;
+ },
+ cancelDrawing: function (this: MeasureControl) {
+ if (!this.options.isDrawingEmpty) {
+ this._measureHandler.disable();
+ this.options.isDrawingEmpty = true;
+
+ this._measureLayers.clearLayers();
+
+ this.options.cbSetDrawingStatus(false);
+ this.options.cbDeleteVisibleShapeById(DRAWING_SHAPE_ID);
+ // this.options.isDrawing = true;
+ }
+ },
+});
+
+// Adds the method to create a new instance of the control
+(L.Control as any).MeasureControl = MeasureControl;
+(L.control as any).measureControl = function (options: any) {
+ return new MeasureControl(options);
+};
diff --git a/libraries/commons/measurements/src/lib/leaflet-control-measurement-extension/index.ts b/libraries/commons/measurements/src/lib/leaflet-control-measurement-extension/index.ts
new file mode 100644
index 0000000000..d772d30f04
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/leaflet-control-measurement-extension/index.ts
@@ -0,0 +1 @@
+export * from "./MeasureControl";
diff --git a/libraries/commons/measurements/src/lib/lib-measurements.tsx b/libraries/commons/measurements/src/lib/lib-measurements.tsx
deleted file mode 100644
index 02831d001a..0000000000
--- a/libraries/commons/measurements/src/lib/lib-measurements.tsx
+++ /dev/null
@@ -1,338 +0,0 @@
-import React, { useState, useEffect, useContext } from "react";
-import { TopicMapContext } from "react-cismap/contexts/TopicMapContextProvider";
-import "leaflet/dist/leaflet.css";
-import "leaflet-draw/dist/leaflet.draw.css";
-import L from "leaflet";
-import "leaflet-draw";
-import "leaflet-editable";
-import "./utils/measure";
-import "./utils/measure-path";
-import "leaflet-measure-path/leaflet-measure-path.css";
-import "./styles/m-style.css";
-import useDeviceDetection from "./hooks/useDeviceDetection";
-import { useMapMeasurementsContext } from "./components/MapMeasurementsProvider";
-import { MapMeasurementProps, MeasurementShapeDrawing } from "..";
-import { MeasurementsSnapping } from "./components/MeasurementsSnapping";
-import { MeasurementStatusDebug } from "./components/MeasurementStatusDebug";
-
-export function Measurements({
- mode: propMode,
- polygonActiveIcon,
- polygonIcon,
- snappingLayers,
-}: Partial & {
- snappingLayers?: any[]; // MapLibre layers for snapping
-}) {
- const { routedMapRef } = useContext(TopicMapContext);
- const {
- mode,
- activeShape,
- setActiveShape,
- shapes,
- setShapes,
- addShape,
- deleteAll,
- setDeleteAll,
- setUpdateShape,
- visibleShapes,
- setVisibleShapes,
- drawingShape: ifDrawing,
- setDrawingShape,
- moveToShape,
- setMoveToShape,
- showAll,
- setShowAll,
- toggleMeasurementMode: toggleUIMode,
- setMapMovingEnd,
- deleteShapeById,
- updateShapeById,
- setLastVisibleShapeActive,
- setDrawingWithLastActiveShape,
- setActiveShapeIfDrawCancelled,
- updateAreaOfDrawing,
- deleteVisibleShapeById,
- setCurrentDrawHandler,
- snappingLatlng,
- config,
- setStatus,
-
- // looks unuseful
- setStartDrawing,
- } = useMapMeasurementsContext();
-
- // Use context mode if propMode is not provided
- const currentMode = propMode !== undefined ? propMode : mode;
-
- const [measureControl, setMeasureControl] = useState(null);
- const [visiblePolylines, setVisiblePolylines] = useState<(string | number)[]>(
- []
- );
- const [drawingShape, setDrawingLine] = useState(null);
-
- const device = useDeviceDetection();
-
- const toggleMeasurementModeHandler = () => {
- toggleUIMode();
- };
-
- useEffect(() => {
- if (routedMapRef?.leafletMap && !measureControl) {
- const mapExample = routedMapRef?.leafletMap?.leafletElement;
- const customOptions = {
- position: "topright",
- // icon_lineActive: makeMeasureActiveIcon,
- // icon_lineInactive: makeMeasureIcon,
- icon_polygonActive: polygonActiveIcon,
- icon_polygonInactive: polygonIcon,
- activeShape,
- mode_btn: ``,
- msj_disable_tool: "Do you want to disable the tool?",
- device,
- shapes,
- snappingLatlng,
- snappingEnabled: config.snappingEnabled,
- cbSaveShape: saveShapeHandler,
- cbUpdateShape: updateShapeHandler,
- cdDeleteShape: deleteShapeHandler,
- cbDeleteVisibleShapeById: deleteVisibleShapeByIdHandler,
- cbVisiblePolylinesChange: visiblePolylinesChange,
- cbSetDrawingStatus: drawingStatusHandler,
- cbSetDrawingShape: drawingShapeHandler,
- measurementOrder: findLargestNumber(shapes),
- measurementMode: currentMode,
- cbSetActiveShape: setActiveShapeHandler,
- cbSetUpdateStatusHandler: setUpdateStatusHandler,
- cbMapMovingEndHandler: mapMovingEndHandler,
- cbSaveLastActiveShapeIdBeforeDrawingHandler:
- saveLastActiveShapeIdBeforeDrawingHandler,
- cbChangeActiveCanceldShapeId: changeActiveCancelledShapeId,
- cbToggleMeasurementMode: toggleMeasurementModeHandler,
- cbUpdateAreaOfDrawingMeasurement: updateAreaOfDrawingMeasurementHandler,
- cbSetCurrentDrawHandler: setCurrentDrawHandler,
- cbSetMapStatus: setStatus,
- };
-
- const measurePolygonControl = (L.control as any).measurePolygon(
- customOptions
- );
- measurePolygonControl.addTo(mapExample);
-
- setMeasureControl(measurePolygonControl);
- }
- }, [routedMapRef]);
-
- useEffect(() => {
- if (measureControl && activeShape) {
- const shapeCoordinates = shapes.filter((s) => s.shapeId === activeShape);
- const map = routedMapRef.leafletMap.leafletElement;
-
- if (ifDrawing) {
- setMoveToShape(null);
- }
-
- if (shapeCoordinates[0]?.shapeId && !ifDrawing && !deleteAll) {
- measureControl.changeColorByActivePolyline(
- map,
- shapeCoordinates[0].shapeId
- );
- }
- if (showAll) {
- const allPolylines = measureControl.getAllPolylines(map);
- measureControl.fitMapToPolylines(map, allPolylines);
- setShowAll(false);
- }
-
- if (deleteAll) {
- setMoveToShape(null);
- measureControl.removePolylineById(map, activeShape);
- const cleanArr = visibleShapes.filter((m) => m.shapeId !== activeShape);
- deleteShapeHandler(activeShape);
- setVisibleShapes(cleanArr);
-
- const cleanAllArr = shapes.filter((m) => m.shapeId !== activeShape);
- setShapes(cleanAllArr);
- setDeleteAll(false);
- if (measureControl.options.shapes.length === 1) {
- measureControl.options.shapes = [];
- }
- const cleanLocalLefletShapes = measureControl.options.shapes.filter(
- (m) => m.shapeId !== activeShape
- );
-
- measureControl.options.shapes = cleanLocalLefletShapes;
- }
- if (moveToShape && !deleteAll) {
- if (shapeCoordinates.length > 0) {
- measureControl.showActiveShape(map, shapeCoordinates[0]?.coordinates);
- }
- }
- }
-
- if (measureControl) {
- const map = routedMapRef.leafletMap.leafletElement;
- measureControl.changeMeasurementMode(currentMode, map);
- const shapeCoordinates = shapes.filter((s) => s.shapeId === activeShape);
- if (shapeCoordinates[0]?.shapeId) {
- measureControl.changeColorByActivePolyline(
- map,
- shapeCoordinates[0].shapeId
- );
- }
-
- if (currentMode === "measurement" && visibleShapes.length === 0) {
- const visibleShapesIds = measureControl.getVisibleShapeIdsArr(
- measureControl._map
- );
- }
- }
- }, [
- activeShape,
- measureControl,
- showAll,
- deleteAll,
- ifDrawing,
- moveToShape,
- currentMode,
- ]);
-
- // keep snappingLatlng and snappingEnabled in sync with control options
- useEffect(() => {
- if (measureControl) {
- try {
- // Update both snappingEnabled and snappingLatlng
- measureControl.options.snappingEnabled = config.snappingEnabled;
- // Force null when snapping is disabled, otherwise use the actual value
- measureControl.options.snappingLatlng = config.snappingEnabled
- ? snappingLatlng
- : null;
- } catch (_) {}
- }
- }, [snappingLatlng, measureControl, config.snappingEnabled]);
-
- useEffect(() => {
- if (measureControl) {
- const cleanedVisibleArr = filterArrByIds(visiblePolylines, shapes);
-
- // Preserve drawing shape (5555) if we're in drawing mode
- const drawingShapeInVisible = visibleShapes.find(
- (s) => s.shapeId === 5555
- );
- if (
- ifDrawing &&
- drawingShapeInVisible &&
- !cleanedVisibleArr.find((s) => s.shapeId === 5555)
- ) {
- cleanedVisibleArr.push(drawingShapeInVisible);
- }
-
- setVisibleShapes(cleanedVisibleArr);
- measureControl.changeMeasurementsArr(shapes);
- }
- }, [visiblePolylines, shapes, ifDrawing]);
-
- useEffect(() => {
- if (drawingShape) {
- const cleanArr = visibleShapes.filter((m) => m.shapeId !== 5555);
- setVisibleShapes([...cleanArr, drawingShape]);
- } else {
- setLastVisibleShapeActive();
- }
- }, [drawingShape]);
-
- const saveShapeHandler = (layer) => {
- addShape(layer);
- };
- const deleteShapeHandler = (id) => {
- deleteShapeById(id);
- };
- const deleteVisibleShapeByIdHandler = (id) => {
- deleteVisibleShapeById(id);
- };
- const updateShapeHandler = (id, newCoordinates, newDistance, newSquare) => {
- updateShapeById(id, newCoordinates, newDistance, newSquare);
- };
-
- const saveLastActiveShapeIdBeforeDrawingHandler = () => {
- setDrawingWithLastActiveShape();
- };
- const changeActiveCancelledShapeId = () => {
- setActiveShapeIfDrawCancelled();
- };
-
- const visiblePolylinesChange = (arr) => {
- setVisiblePolylines(arr);
- };
-
- const drawingStatusHandler = (status) => {
- setDrawingShape(status);
- setStartDrawing(status);
- };
-
- const drawingShapeHandler = (draw) => {
- setDrawingLine(draw);
- };
- const setActiveShapeHandler = (id) => {
- setActiveShape(id);
- setMoveToShape(null);
- };
- const setUpdateStatusHandler = (status) => {
- setUpdateShape(status);
- };
- const mapMovingEndHandler = (status) => {
- setMapMovingEnd(status);
- };
-
- const updateAreaOfDrawingMeasurementHandler = (newArea) => {
- updateAreaOfDrawing(newArea);
- };
-
- console.debug("RENDER: [MAPMEASUREMENT] MapMeasurement");
-
- return (
- <>
-
-
- {currentMode === "measurement" && (
-
- )}
- >
- );
-}
-
-function filterArrByIds(
- arrIds: (string | number)[],
- fullArray: MeasurementShapeDrawing[]
-): MeasurementShapeDrawing[] {
- const finalResult: MeasurementShapeDrawing[] = [];
- fullArray.forEach((currentItem) => {
- if (arrIds.includes(currentItem.shapeId)) {
- finalResult.push(currentItem);
- }
- });
-
- return finalResult;
-}
-
-function findLargestNumber(measurements: MeasurementShapeDrawing[]): number {
- let largestNumber = 0;
-
- measurements.forEach((item) => {
- if (item.number > largestNumber) {
- largestNumber = item.number;
- }
- });
-
- return largestNumber;
-}
-
-/**
- * @deprecated Use `Measurements` instead. This component will be removed in a future version.
- */
-export function MapMeasurementsObjects(
- props: Partial & {
- snappingEnabled?: boolean;
- snappingLayer?: any;
- }
-) {
- return ;
-}
diff --git a/libraries/commons/measurements/src/lib/snapping/types.ts b/libraries/commons/measurements/src/lib/snapping/types.ts
deleted file mode 100644
index f5828d5c2e..0000000000
--- a/libraries/commons/measurements/src/lib/snapping/types.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export interface SnappingPoint {
- coordinates: [number, number]; // [lng, lat]
- sourceId: string;
- distance?: number; // Calculated later
- metadata?: {
- featureId?: string;
- shapeId?: string | number;
- geometryType?: string;
- };
-}
-
-export interface MousePosition {
- x: number;
- y: number;
-}
-
-export interface SnappingContext {
- mousePosition: MousePosition;
- queryRadius: number;
- maplibreMap: any;
- leafletMap: any;
-}
diff --git a/libraries/commons/measurements/src/lib/snapping/utils/coordinateExtraction.ts b/libraries/commons/measurements/src/lib/snapping/utils/coordinateExtraction.ts
index 8cf198d841..c722445be5 100644
--- a/libraries/commons/measurements/src/lib/snapping/utils/coordinateExtraction.ts
+++ b/libraries/commons/measurements/src/lib/snapping/utils/coordinateExtraction.ts
@@ -1,4 +1,4 @@
-import { SnappingPoint } from "../types";
+import { SnappingPoint } from "../../types";
/**
* Extract snapping points from a GeoJSON geometry
diff --git a/libraries/commons/measurements/src/lib/snapping/utils/mapLibreExtraction.ts b/libraries/commons/measurements/src/lib/snapping/utils/mapLibreExtraction.ts
new file mode 100644
index 0000000000..00c3eac41d
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/snapping/utils/mapLibreExtraction.ts
@@ -0,0 +1,68 @@
+import {
+ type LayerSpecification,
+ Map as MapLibreMap,
+ type MapGeoJSONFeature,
+ Point,
+ type PointLike,
+} from "maplibre-gl";
+import { LayerCarmaConf } from "@carma/types";
+import { SnappingPoint } from "../../types";
+import { extractPointsFromGeometry } from "./coordinateExtraction";
+
+export const getSnappingPointsFromMapLibre = (
+ maps: MapLibreMap[],
+ screenPoint: { x: number; y: number },
+ radius: number
+): SnappingPoint[] => {
+ const coordinatePoints: SnappingPoint[] = [];
+
+ maps.forEach((currentMaplibreMap) => {
+ if (
+ currentMaplibreMap &&
+ currentMaplibreMap.getStyle &&
+ currentMaplibreMap.getCanvas
+ ) {
+ try {
+ const style = currentMaplibreMap.getStyle();
+ if (style && style.layers) {
+ const canvas = currentMaplibreMap.getCanvas();
+ const rect = canvas.getBoundingClientRect();
+
+ // Calculate point relative to this map's canvas
+ const pointX = screenPoint.x - rect.left;
+ const pointY = screenPoint.y - rect.top;
+
+ // Use Point objects directly to avoid internal conversion overhead in MapLibre
+ const bbox: [PointLike, PointLike] = [
+ new Point(pointX - radius, pointY - radius),
+ new Point(pointX + radius, pointY + radius),
+ ];
+
+ const features = currentMaplibreMap.queryRenderedFeatures(bbox, {
+ layers: style.layers
+ .filter((layer: LayerSpecification) => {
+ // Skip layers with skipSnapping metadata
+ const skipSnapping = (
+ (layer.metadata as any)?.carmaConf as LayerCarmaConf
+ )?.skipSnapping;
+ return !skipSnapping && !layer.id.startsWith("highlight-");
+ })
+ .map((layer: LayerSpecification) => layer.id),
+ });
+
+ features.forEach((feature: MapGeoJSONFeature) => {
+ const points = extractPointsFromGeometry(
+ feature.geometry,
+ "vector-features"
+ );
+ coordinatePoints.push(...points);
+ });
+ }
+ } catch (error) {
+ console.warn("Error extracting vector features:", error);
+ }
+ }
+ });
+
+ return coordinatePoints;
+};
diff --git a/libraries/commons/measurements/src/lib/types/MeasurementLayer.ts b/libraries/commons/measurements/src/lib/types/MeasurementLayer.ts
new file mode 100644
index 0000000000..fbf3fa7f8c
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/types/MeasurementLayer.ts
@@ -0,0 +1,11 @@
+import { Layer, LatLng } from "@carma/leaflet";
+
+export interface MeasurementLayer extends Omit {
+ customID?: number | string | symbol;
+ customHandle?: number;
+ _path?: SVGPathElement;
+ enableEdit?: () => void;
+ disableEdit?: () => void;
+ getLatLng?: () => LatLng;
+ _leaflet_id?: number;
+}
diff --git a/libraries/commons/measurements/src/lib/types/MeasurementLeafletEvent.ts b/libraries/commons/measurements/src/lib/types/MeasurementLeafletEvent.ts
new file mode 100644
index 0000000000..0d958f568b
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/types/MeasurementLeafletEvent.ts
@@ -0,0 +1,6 @@
+import { LeafletEvent, LayerGroup } from "@carma/leaflet";
+
+export interface MeasurementLeafletEvent extends LeafletEvent {
+ layerType?: string;
+ layers?: LayerGroup;
+}
diff --git a/libraries/commons/measurements/src/lib/types/MeasurementPolygon.ts b/libraries/commons/measurements/src/lib/types/MeasurementPolygon.ts
new file mode 100644
index 0000000000..fd9b2ae761
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/types/MeasurementPolygon.ts
@@ -0,0 +1,10 @@
+import { Polygon } from "@carma/leaflet";
+
+export interface MeasurementPolygon extends Omit {
+ customID?: number | string | symbol;
+ customShape?: string;
+ customHandle?: number;
+ _path?: SVGPathElement;
+ enableEdit?: () => void;
+ disableEdit?: () => void;
+}
diff --git a/libraries/commons/measurements/src/lib/types/MeasurementPolyline.ts b/libraries/commons/measurements/src/lib/types/MeasurementPolyline.ts
new file mode 100644
index 0000000000..616bf901de
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/types/MeasurementPolyline.ts
@@ -0,0 +1,10 @@
+import { Polyline } from "@carma/leaflet";
+
+export interface MeasurementPolyline extends Omit {
+ customID?: number | string | symbol;
+ customShape?: string;
+ _path?: SVGPathElement;
+ _leaflet_id?: number;
+ enableEdit?: () => void;
+ disableEdit?: () => void;
+}
diff --git a/libraries/commons/measurements/src/lib/types/MeasurementShape.d.ts b/libraries/commons/measurements/src/lib/types/MeasurementShape.d.ts
new file mode 100644
index 0000000000..e20434aafa
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/types/MeasurementShape.d.ts
@@ -0,0 +1,15 @@
+export interface MeasurementShapeDrawing {
+ shapeId: number | string;
+ number: number;
+ coordinates?: unknown;
+ [key: string]: unknown;
+}
+
+export interface MeasurementShape {
+ shapeId: number | string;
+ distance?: number;
+ area?: number;
+ customTitle?: string;
+ shapeType?: "line" | "polygon" | string;
+ [key: string]: unknown;
+}
diff --git a/libraries/commons/measurements/src/lib/types/MeasurementShapeData.ts b/libraries/commons/measurements/src/lib/types/MeasurementShapeData.ts
new file mode 100644
index 0000000000..bf44907c58
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/types/MeasurementShapeData.ts
@@ -0,0 +1,15 @@
+export interface MeasurementShapeData {
+ coordinates: number[][];
+ options: {
+ color: string;
+ fillColor: string | null;
+ opacity: number;
+ weight: number;
+ };
+ shapeId: number | string | symbol;
+ distance: string;
+ number: number;
+ area?: string | null;
+ shapeType: "line" | "polygon";
+ customTitle?: string;
+}
diff --git a/libraries/commons/measurements/src/lib/types/SnappingPoint.d.ts b/libraries/commons/measurements/src/lib/types/SnappingPoint.d.ts
new file mode 100644
index 0000000000..a2bf253c55
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/types/SnappingPoint.d.ts
@@ -0,0 +1,10 @@
+export type SnappingPoint = {
+ coordinates: [number, number]; // [lng, lat]
+ sourceId: string;
+ distance?: number; // Calculated later
+ metadata?: {
+ featureId?: string;
+ shapeId?: string | number;
+ geometryType?: string;
+ };
+};
diff --git a/libraries/commons/measurements/src/lib/types/index.ts b/libraries/commons/measurements/src/lib/types/index.ts
new file mode 100644
index 0000000000..76d2486e83
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/types/index.ts
@@ -0,0 +1,8 @@
+// Leaflet extensions (large type augmentation)
+export * from "./MeasurementPolyline";
+export * from "./MeasurementPolygon";
+export * from "./MeasurementLayer";
+export * from "./MeasurementLeafletEvent";
+export * from "./MeasurementShapeData";
+export * from "./SnappingPoint";
+export * from "./MeasurementShape";
diff --git a/libraries/commons/measurements/src/lib/utils/constants.ts b/libraries/commons/measurements/src/lib/utils/constants.ts
new file mode 100644
index 0000000000..768507eec7
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/utils/constants.ts
@@ -0,0 +1 @@
+export const DRAWING_SHAPE_ID = Symbol("active-drawing-shape");
diff --git a/libraries/commons/measurements/src/lib/utils/helper.ts b/libraries/commons/measurements/src/lib/utils/helper.ts
deleted file mode 100644
index 8c06ce04d8..0000000000
--- a/libraries/commons/measurements/src/lib/utils/helper.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import localforage from "localforage";
-
-export const setFromLocalforage = async (
- lfKey: string,
- setter: (value: any) => void,
- fallbackValue?: any,
- forceFallback?: boolean
-) => {
- try {
- const value = await localforage.getItem(lfKey);
- if (value !== undefined && value !== null) {
- setter(value);
- } else if (fallbackValue !== undefined || forceFallback === true) {
- setter(fallbackValue);
- }
- } catch (error) {
- console.warn(`Failed to load ${lfKey} from localStorage:`, error);
- if (fallbackValue !== undefined || forceFallback === true) {
- setter(fallbackValue);
- }
- }
-};
-
-export const saveToLocalforage = async (lfKey: string, value: any) => {
- try {
- await localforage.setItem(lfKey, value);
- } catch (error) {
- console.warn(`Failed to save ${lfKey} to localStorage:`, error);
- }
-};
-
-export const adjustClickPosition = (
- domEvent: MouseEvent,
- closestPoint: any,
- eventType: string,
- leafletMap: any,
- currentDrawHandler?: any
-) => {
- const containerPoint = leafletMap.mouseEventToContainerPoint(domEvent);
- const shiftedContainerPoint = L.point(containerPoint.x, containerPoint.y);
- // Use closestPoint if available, otherwise use shifted click position
- if (!closestPoint) {
- return false;
- }
-
- const [lng, lat] = closestPoint.geometry.coordinates;
- const finalLatLng = L.latLng(lat, lng);
-
- // Check if we're drawing and snapped to first vertex (polygon closure)
- // ONLY trigger if the snap source is the drawing-in-progress (not external features)
- if (
- currentDrawHandler &&
- currentDrawHandler._poly &&
- currentDrawHandler._poly._latlngs
- ) {
- const latlngs = currentDrawHandler._poly._latlngs;
- if (latlngs.length >= 3) {
- const firstVertex = latlngs[0];
- const snappedCoord = closestPoint.geometry.coordinates;
- // Trigger polygon closure if snapped coordinates EXACTLY match the first vertex
- // Use very tight threshold (1e-10) to ensure it's the exact same point, not just nearby
- const exactMatchThreshold = 1e-10;
- if (
- Math.abs(snappedCoord[1] - firstVertex.lat) < exactMatchThreshold &&
- Math.abs(snappedCoord[0] - firstVertex.lng) < exactMatchThreshold
- ) {
- // Try to find and click the first vertex marker directly
- // The vertex markers are in _markers array with customHandle property
- if (
- currentDrawHandler._markers &&
- currentDrawHandler._markers.length > 0
- ) {
- const firstMarker = currentDrawHandler._markers[0];
- if (firstMarker) {
- // Fire click event on the first vertex marker, not the map
- firstMarker.fire("click", {
- latlng: firstVertex,
- target: firstMarker,
- });
- return true; // Don't fire synthetic map event
- }
- }
- }
- }
- }
-
- // Fire a new click event with shifted coordinates on the map
- leafletMap.fire(eventType, {
- latlng: finalLatLng,
- containerPoint: shiftedContainerPoint,
- originalEvent: domEvent,
- });
-
- return false;
-};
-
-// Prepare a Leaflet LatLng from a GeoJSON Point-like feature with coordinates [lng, lat]
-export const toLatLngFromClosestPoint = (closestPoint: any) => {
- if (
- !closestPoint ||
- !closestPoint.geometry ||
- !closestPoint.geometry.coordinates
- ) {
- return null;
- }
- const [lng, lat] = closestPoint.geometry.coordinates;
- return L.latLng(lat, lng);
-};
diff --git a/libraries/commons/measurements/src/lib/utils/measure-path.js b/libraries/commons/measurements/src/lib/utils/measure-path.js
deleted file mode 100644
index 0896dbd1de..0000000000
--- a/libraries/commons/measurements/src/lib/utils/measure-path.js
+++ /dev/null
@@ -1,1154 +0,0 @@
-// Create a class for the plugin
-L.Control.MeasurePolygon = L.Control.extend({
- options: {
- position: "topright",
- icon_lineActive: "https://img.icons8.com/?size=48&id=98497&format=png",
- icon_lineInactive: "https://img.icons8.com/?size=48&id=98463&format=png",
- icon_polygonActive: "https://img.icons8.com/?size=48&id=98497&format=png",
- icon_polygonInactive: "https://img.icons8.com/?size=48&id=98463&format=png",
- html_template: `Results
-Area:
_p_area
-Perimeter :
_p_perimetro
`,
- height: 130,
- width: 150,
- mode_btn: "",
- color_polygon: "black",
- fillColor_polygon: "yellow",
- weight_polygon: "2",
- checkonedrawpoligon: false,
- changeModeButtonActive: false,
- msj_disable_tool: "Möchten Sie das Tool deaktivieren?",
- shapes: [],
- activeShape: null,
- shapeMode: "line",
- measurementOrder: 0,
- moveToShape: false,
- cb: function () {
- console.debug("Callback function executed!");
- },
- cbSaveShape: function () {
- console.debug("Callback function executed!");
- },
- cdDeleteShape: function () {
- console.debug("Callback function executed!");
- },
- cbUpdateShape: function () {
- console.debug("Callback function executed!");
- },
- cbVisiblePolylinesChange: function () {
- console.debug("Callback function executed!");
- },
- cbSetDrawingStatus: function () {
- console.debug("Callback function executed!");
- },
- cbSetDrawingShape: function () {
- console.debug("Callback function executed!");
- },
- cbSetActiveShape: function () {
- console.debug("Callback function executed!");
- },
- cbSetUpdateStatusHandler: function () {
- console.debug("Callback function executed!");
- },
- cbMapMovingEndHandler: function () {
- console.debug("Callback function executed!");
- },
- cbSaveLastActiveShapeIdBeforeDrawingHandler: function () {
- console.debug("Callback function executed!");
- },
- cbChangeActiveCanceldShapeId: function () {
- console.debug("Callback function executed!");
- },
- cbToggleMeasurementMode: function () {
- console.debug("Callback function executed!");
- },
- cbGetMeasurementModeHandler: function () {
- console.debug("Callback function executed!");
- },
- cbDeleteVisibleShapeById: function () {
- console.debug("Callback function executed!");
- },
- cbUpdateAreaOfDrawingMeasurement: function () {
- console.debug("Callback function executed!");
- },
- cbSetCurrentDrawHandler: function () {
- console.debug("Callback function executed!");
- },
- visiblePolylines: [],
- localShapeStore: [],
- ifDrawing: false,
- nativeMove: false,
- currenLine: null,
- polygonMode: false,
- measurementMode: false,
- startDrawing: false,
- customTooltip: null,
- device: null,
- clickAfterShapeSelection: false,
- snappingLatlng: null,
- snappingEnabled: true,
- },
-
- drawingPolygons: function (map) {
- this.options.shapeMode = "polygon";
- this._measureHandler = new L.Draw.Polygon(map, {
- showArea: true,
- shapeOptions: {
- color: "#267bdcd4",
- fillColor: null,
- fillOpacity: 0.2,
- stroke: true,
- },
- });
-
- L.drawLocal.draw.handlers.polygon.tooltip.start =
- "Klicken, um den Startpunkt der Messung zu setzen.";
- L.drawLocal.draw.handlers.polygon.tooltip.cont =
- "Klicken (ggf. mehrmals), um die nächsten Punkte des Linienzuges zu setzen.";
- L.drawLocal.draw.handlers.polygon.tooltip.end = `Zum Beenden auf den letzten angelegt Punkt klicken.
- Zum Messen einer Fläche auf den ersten angeleten.
- Punkt klicken und die Fläche so schließen`;
-
- this._toggleMeasure(
- "img_plg_measure_polygon",
- "icon_polygonActive",
- "icon_polygonInactive"
- );
- },
-
- drawingLines: function (map, event) {
- if (this.options.customTooltip) {
- this.options.customTooltip.style.visibility = "hidden";
- }
- this.options.shapeMode = "line";
- this._measureHandler = new L.Draw.Polyline(map, {
- showLength: true,
- shapeOptions: {
- weight: 3,
- color: "#267bdcd4",
- opacity: 1,
- },
- });
-
- this.options.currenLine = this._measureHandler;
- this.options.cbSetCurrentDrawHandler(this._measureHandler);
-
- const tooltipContent = `
-
-
Zum Beenden auf den letzten angelegten Punkt klicken.
-
Zum Messen einer Fläche auf den ersten angelegten Punkt klicken und die Fläche so schließen.
-
-`;
-
- L.drawLocal.draw.handlers.polyline.tooltip.start =
- "Klicken, um den Startpunkt der Messung zu setzen.";
- L.drawLocal.draw.handlers.polyline.tooltip.cont =
- "Klicken (ggf. mehrmals), um die nächsten Punkte des Linienzuges zu setzen.";
- L.drawLocal.draw.handlers.polyline.tooltip.end = tooltipContent;
-
- this._measureHandler.enable();
-
- // Store original click position before Leaflet.Draw modifies it
- this._lastOriginalClick = null;
-
- const storeClickPosition = (e) => {
- if (this._measureHandler && this._measureHandler._enabled) {
- // Store the ORIGINAL click position
- this._lastOriginalClick = {
- latlng: L.latLng(e.latlng.lat, e.latlng.lng),
- containerPoint: map.latLngToContainerPoint(e.latlng),
- };
- }
- };
-
- map.on("click", storeClickPosition);
- map.on("touchstart", storeClickPosition); // Also handle touch events on mobile
- const latlng =
- this.options.snappingEnabled && this.options.snappingLatlng
- ? this.options.snappingLatlng
- : event.latlng;
-
- this.options.currenLine.addVertex(latlng);
-
- const tooltip = document.querySelector(".leaflet-draw-tooltip");
-
- const pos = map.latLngToLayerPoint(latlng);
- L.DomUtil.setPosition(tooltip, pos);
-
- this._toggleMeasure(
- "img_plg_lines",
- "icon_lineActive",
- "icon_lineInactive"
- );
- },
-
- startDrawing: function () {
- this.options.startDrawing = true;
- },
-
- saveShapeHandler: function (layer, distance = null, area = null, map) {
- const latlngs = layer.getLatLngs();
- const latlngsJSON = layer.toGeoJSON();
- const shapeId = layer._leaflet_id;
- layer.customID = shapeId;
- layer.on("click", () => {
- this.options.cbSetActiveShape(layer.customID);
- this.options.cbSetUpdateStatusHandler(false);
- });
-
- if (this.options.shapeMode === "polygon") {
- const polygon = this.replaceLineToPolygon(map, layer);
- this.options.cbSaveShape(polygon);
- this.getVisibleShapeIdsArr(map);
- } else {
- const prepeareCoordinates =
- this.options.shapeMode === "line"
- ? latlngsJSON.geometry.coordinates
- : latlngsJSON.geometry.coordinates[0];
- const reversedCoordinates = prepeareCoordinates.map((item) => {
- return item.reverse();
- });
-
- const preparePolygon = {
- coordinates: reversedCoordinates,
- options: {
- color: "#267bdcd4",
- fillColor: null,
- opacity: 0.5,
- weigt: 4,
- },
- shapeId,
- distance,
- number: this.options.measurementOrder,
- area,
- shapeType: this.options.shapeMode,
- };
- this.options.cbSaveShape(preparePolygon);
- this.getVisibleShapeIdsArr(map);
- }
- },
-
- _onPolylineDrag: function (event) {
- if (this.options.customTooltip) {
- this.options.customTooltip.style.visibility = "hidden";
- }
- this.options.cbSetUpdateStatusHandler(true);
-
- // Set status based on drag type
- if (this.options.cbSetMapStatus) {
- if (
- event.type === "editable:drag" ||
- event.type === "editable:dragstart"
- ) {
- // Dragging whole shape
- this.options.cbSetMapStatus("MOVING");
- } else if (event.type === "editable:vertex:drag") {
- // Dragging vertices or deleting vertex
- this.options.cbSetMapStatus("EDITING");
- }
- }
-
- const polyline = event.target;
- const layer = event.layer;
- this.options.cbSetActiveShape(layer.customID);
- const latlngsJSON = layer.toGeoJSON();
- const isLine = layer.toGeoJSON().geometry.type === "LineString";
- const prepeareCoordinates = isLine
- ? latlngsJSON.geometry.coordinates
- : latlngsJSON.geometry.coordinates[0];
- const reversedCoordinates = prepeareCoordinates.map((item) => {
- return item.reverse();
- });
-
- const square = !isLine ? this.calculateArea(reversedCoordinates) : null;
- polyline.updateMeasurements();
- const newDistance = this._UpdateDistance(layer);
- const shapeId = polyline?.customID
- ? polyline?.customID
- : polyline._leaflet_id;
-
- this.options.cbUpdateShape(
- shapeId,
- reversedCoordinates,
- newDistance,
- square
- );
- this.options.checkonedrawpoligon = false;
- },
-
- _onPolygonClick: function (map, event) {
- const clickedPolygon = event.target;
- const latlngs = clickedPolygon.getLatLngs();
-
- this._measureLayers.removeLayer(clickedPolygon._leaflet_id);
- const shapeId = clickedPolygon?.customID
- ? clickedPolygon?.customID
- : clickedPolygon._leaflet_id;
-
- this.options.cdDeleteShape(shapeId, this.options.localShapeStore);
-
- const allPolyLines = this.getVisiblePolylines(map);
- this.getVisiblePolylinesIds(allPolyLines);
- },
-
- onAdd: function (map) {
- const linesContainer = L.DomUtil.create(
- "div",
- "leaflet-bar leaflet-control dont-show m-container"
- );
-
- // const modeBtn = L.DomUtil.create(
- // "div",
- // "leaflet-bar leaflet-control dont-show m-container hide-draw-btn draw-custom-button",
- // linesContainer,
- // );
-
- // modeBtn.id = "draw_shape";
- // modeBtn.title = "Flächen- und Umfangsmessungen";
-
- // modeBtn.innerHTML = this.options.mode_btn;
-
- const lineIcon = L.DomUtil.create("a", "", linesContainer);
- lineIcon.innerHTML = `
-
-

-
- `;
- lineIcon.href = "#";
- lineIcon.title = "Messmodus";
-
- const iconsWrapper = L.DomUtil.create("div", "m-icons-wrapper");
- iconsWrapper.appendChild(linesContainer);
-
- L.DomEvent.on(
- lineIcon,
- "click",
- (event) => {
- event.preventDefault(); // Prevent default action (e.g., redirection)
- this.toggleMeasurementMode();
- },
- this
- );
-
- this._map = map;
-
- this._measureLayers = L.layerGroup().addTo(map);
-
- map.on("click", (event) => {
- const mode = this.options.measurementMode;
- if (!this.options.checkonedrawpoligon && mode === "measurement") {
- this.drawingLines(map, event);
- this.options.checkonedrawpoligon = true;
- } else {
- // this.options.checkonedrawpoligon = false;
- }
-
- if (this.options.clickAfterShapeSelection) {
- this.options.checkonedrawpoligon = false;
- this.options.clickAfterShapeSelection = false;
- }
- });
-
- map.on("draw:created", (event) => {
- this.options.checkonedrawpoligon = false;
- this.options.ifDrawing = false;
-
- this.options.cbSetDrawingStatus(false);
- this.options.cbSetDrawingShape(null);
-
- const layer = event.layer;
- // layer.on("dblclick", this._onPolygonClick.bind(this, map));
-
- layer.on("editable:vertex:dragend", () => {
- this.options.cbSetUpdateStatusHandler(false);
- // Reset status to WAITING when vertex editing ends
- if (this.options.cbSetMapStatus) {
- this.options.cbSetMapStatus("WAITING");
- }
- });
-
- // Reset status to WAITING when drag ends
- layer.on("editable:dragend", () => {
- if (this.options.cbSetMapStatus) {
- this.options.cbSetMapStatus("WAITING");
- }
- });
-
- // Add style to polygon
- layer.addTo(this._measureLayers).showMeasurements().enableEdit();
- layer.options.draggable = false;
-
- const distance = this._UpdateDistance(layer);
-
- this.saveShapeHandler(layer, distance, null, map);
-
- layer.on(
- "editable:drag editable:vertex:drag editable:vertex:deleted editable:dragstart editable:dragend",
- this._onPolylineDrag.bind(this)
- );
-
- this.options.checkonedrawpoligon = false;
-
- this._measureHandler.disable();
- });
-
- map.on("draw:drawstart", (event) => {
- const mouseActive =
- L.Browser.touch && matchMedia("(hover:hover)").matches;
- if (
- mouseActive ||
- event.layerType === "circle" ||
- event.layerType === "rectangle"
- ) {
- event.target.touchExtend.enable();
- } else {
- event.target.touchExtend.disable();
- }
- this.options.cbSaveLastActiveShapeIdBeforeDrawingHandler();
- this.options.measurementOrder = this.options.measurementOrder + 1;
- const shapesObj = {
- coordinates: [[51.352635, 7.209284]],
- distance: 0,
- shapeId: 5555,
- number: this.options.measurementOrder,
- shapeType: "line",
- };
- this.changeColorByActivePolyline(map, "ddfsc1231");
- });
-
- map.on("draw:drawvertex", (event) => {
- const layers = event.layers;
- const latlngs = [];
- let index = 0;
- let firsHovering = false;
-
- layers.eachLayer((layer) => {
- const markerLatLng = layer.getLatLng();
- layer.customHandle = index++;
-
- // Store reference to the click handler so we can check conditions
- const vertexClickHandler = (e) => {
- const clickLatLng = e.latlng;
- const markerLatLng = e.target.getLatLng();
-
- // Use the ORIGINAL click position stored in map click handler
- const originalClick = this._lastOriginalClick;
-
- // Calculate pixel distance using ORIGINAL click position
- const clickPoint = originalClick
- ? originalClick.containerPoint
- : this._map.latLngToContainerPoint(clickLatLng);
- const markerPoint = this._map.latLngToContainerPoint(markerLatLng);
- const pixelDistance = Math.sqrt(
- Math.pow(clickPoint.x - markerPoint.x, 2) +
- Math.pow(clickPoint.y - markerPoint.y, 2)
- );
-
- // Only process first vertex clicks (for closing polygon)
- if (e.target.customHandle !== 0) {
- return; // Not the first vertex, let Leaflet.Draw handle it normally
- }
-
- // Only apply distance check when snapping is enabled (desktop)
- // On mobile, snapping is disabled so let Leaflet.Draw handle closure normally
- if (this.options.snappingEnabled) {
- // Check if original click is within snapping radius OR if snapping is active and brought us here
- const maxClickDistance = this.options.snappingQueryRadius || 40;
- const isWithinSnappingRadius = pixelDistance <= maxClickDistance;
- const isSnappedToFirstVertex =
- this.options.snappingLatlng &&
- Math.abs(this.options.snappingLatlng.lat - markerLatLng.lat) <
- 0.0000001 &&
- Math.abs(this.options.snappingLatlng.lng - markerLatLng.lng) <
- 0.0000001;
-
- if (!isWithinSnappingRadius && !isSnappedToFirstVertex) {
- // Don't process this click - it shouldn't close the polygon
- return;
- }
- }
- // On mobile (snapping disabled), always allow closure when clicking first vertex
- this.options.shapeMode = "polygon";
- this.options.currenLine.completeShape();
- };
-
- layer.on("click", vertexClickHandler);
- layer.on("mouseover", (e) => {
- const coordinates = this._measureHandler._poly._latlngs;
- const latLngArray = coordinates.map((c) => [c.lat, c.lng]);
- latLngArray.push(latLngArray[0]);
- const area = this.calculateArea(latLngArray);
- // this.options.customTooltip.style.visibility = "hidden";
- if (e.target.customHandle === 0 && firsHovering) {
- this.options.cbUpdateAreaOfDrawingMeasurement(area);
- L.drawLocal.draw.handlers.polyline.tooltip.end = `Den Startpunkt anklicken, um die Fläche zu schließen.`;
- }
- firsHovering = true;
- });
-
- layer.on("mouseout", (e) => {
- if (e.target.customHandle === 0) {
- const tooltipContent = `
-
-
Zum Beenden auf den letzten angelegten Punkt klicken.
-
Zum Messen einer Fläche auf den ersten angelegten Punkt klicken und die Fläche so schließen.
-
- `;
- L.drawLocal.draw.handlers.polyline.tooltip.end = tooltipContent;
-
- this.options.cbUpdateAreaOfDrawingMeasurement(null);
- }
- });
-
- const latLng = layer.getLatLng();
- latlngs.push(latLng);
- if (index === 1) {
- L.drawLocal.draw.handlers.polyline.tooltip.end = `
- Den Endpunkt erneut anklicken,
-
um die Streckenmessung zu beenden.
`;
- }
- if (index > 2) {
- L.drawLocal.draw.handlers.polyline.tooltip.end = `
- Den Endpunkt erneut anklicken, um die Streckenmessung zu beenden.
- Zum Messen einer Fläche erneut auf den Startpunkt klicken.
`;
- }
- });
-
- const formatPerimeter = this.calculateDistance(latlngs);
- const distance = this.formatDistance(formatPerimeter);
-
- if (!this.options.ifDrawing) {
- const shapesObj = {
- coordinates: [latlngs],
- distance,
- shapeId: 5555,
- number: this.options.measurementOrder,
- shapeType: "line",
- };
-
- this.options.ifDrawing = true;
- this.options.cbSetDrawingStatus(true);
- // this.options.cbSaveShape(shapesObj);
- this.options.cbSetDrawingShape(shapesObj);
- } else {
- const shapesObj = {
- coordinates: [latlngs],
- distance,
- shapeId: 5555,
- shapeType: "line",
- number: this.options.measurementOrder,
- };
- this.options.cbSetDrawingShape(shapesObj);
- }
- });
-
- map.on("draw:canceled", () => {
- this.options.checkonedrawpoligon = true;
- this.options.cbSetDrawingStatus(false);
-
- this._measureHandler.disable();
-
- this._toggleMeasure(
- "img_plg_lines",
- "icon_lineActive",
- "icon_lineInactive"
- );
-
- this.options.cbDeleteVisibleShapeById(5555);
-
- this.options.cbChangeActiveCanceldShapeId();
- });
-
- map.on("moveend", () => {
- const allPolyLines = this.getVisiblePolylines(map);
- this.getVisiblePolylinesIds(allPolyLines);
- this.options.cbMapMovingEndHandler(true);
- this.options.cbSetUpdateStatusHandler(false);
- });
-
- map.on("mousemove", (event) => {
- const target = event.originalEvent.target;
- const isDesktop = this.options.device === "Desktop" ? true : false;
- const mode = this.options.measurementMode;
- // this._propagateEventToUnderlyingLayers(map, event, "mouseover");
-
- if (isDesktop) {
- if (!this.options.customTooltip && mode === "measurement") {
- const popupPane = map._panes.popupPane;
-
- this.options.customTooltip = L.DomUtil.create(
- "div",
- "leaflet-draw-custom-tooltip",
- popupPane
- );
-
- this.options.customTooltip.innerHTML = `Klicken, um den Startpunkt der Messung zu setzen.
`;
- this.options.customTooltip.style.visibility = "inherit";
-
- const pos = this._map.latLngToLayerPoint(event.latlng);
- L.DomUtil.setPosition(this.options.customTooltip, pos);
- }
-
- if (this.options.customTooltip && mode === "measurement") {
- const pos = this._map.latLngToLayerPoint(event.latlng);
- // const offsetX = 20;
- const offsetX = 0;
- // L.DomUtil.setPosition(this.options.customTooltip, pos);
- L.DomUtil.setPosition(this.options.customTooltip, {
- x: pos.x + offsetX,
- y: pos.y,
- });
- if (target.classList.contains("leaflet-div-icon")) {
- this.options.customTooltip.style.visibility = "hidden";
- }
- if (
- (target.classList.contains("leaflet-container") ||
- target.classList.contains("leaflet-gl-layer")) &&
- !this.options.ifDrawing
- ) {
- this.options.customTooltip.style.visibility = "visible";
- }
- }
- }
- });
-
- map.on("mouseout", (event) => {
- if (this.options.customTooltip) {
- this.options.customTooltip.style.visibility = "hidden";
- }
- });
-
- return iconsWrapper;
- },
-
- // _propagateEventToUnderlyingLayers: function (map, event, eventType) {
- // // Get the lat/lng of the vertex
- // // const latlng = event.target.getLatLng();
- // const latlng =
- // event.target && event.target.getLatLng
- // ? event.target.getLatLng() // For vertex marker events
- // : event.latlng; // For map mousemove events
-
- // // Convert to container point
- // const point = map.latLngToContainerPoint(latlng);
-
- // // Find all layers at this point (excluding the measurement vertex itself)
- // const layers = [];
- // map.eachLayer((layer) => {
- // // Skip the measurement layers and the current target
- // if (layer === event.target || layer === this._measureLayers) {
- // return;
- // }
-
- // // Check if this layer is a GeoJSON or similar feature layer with mouseover handlers
- // if (
- // layer.feature &&
- // layer._events &&
- // (layer._events.mouseover || layer._events.mouseout)
- // ) {
- // let isInside = false;
-
- // // For polygon/polyline layers, check if point is inside
- // if (layer.getBounds) {
- // const bounds = layer.getBounds();
- // if (bounds.contains(latlng)) {
- // // For polygons, do a more precise check using Leaflet's internal method
- // if (
- // layer instanceof L.Polygon ||
- // (layer._latlngs && Array.isArray(layer._latlngs))
- // ) {
- // // Use a simple point-in-polygon check
- // // For now, just use bounds check as a proxy
- // isInside = true;
- // } else {
- // isInside = true;
- // }
- // }
- // } else if (layer.getLatLng) {
- // // For point markers
- // isInside = layer.getLatLng().equals(latlng);
- // }
-
- // if (isInside) {
- // layers.push(layer);
- // }
- // }
- // });
-
- // // Fire the event on the underlying layers
- // layers.forEach((layer) => {
- // if (eventType === "mouseover" && layer._events.mouseover) {
- // layer.fire("mouseover", {
- // latlng: latlng,
- // layerPoint: point,
- // containerPoint: point,
- // originalEvent: event.originalEvent,
- // target: layer,
- // });
- // } else if (eventType === "mouseout" && layer._events.mouseout) {
- // layer.fire("mouseout", {
- // latlng: latlng,
- // layerPoint: point,
- // containerPoint: point,
- // originalEvent: event.originalEvent,
- // target: layer,
- // });
- // }
- // });
- // },
-
- _UpdateAreaPerimetro: function (layer) {
- const latlngs = layer.getLatLngs()[0];
-
- const options = {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- };
- },
-
- _UpdateDistance: function (layer) {
- let totalDistance = 0;
- const isLine = layer.toGeoJSON().geometry.type === "LineString";
- const latlngs = isLine ? layer.getLatLngs() : layer.getLatLngs()[0];
-
- if (!isLine) {
- latlngs.push(latlngs[0]);
- }
-
- for (let i = 0; i < latlngs.length - 1; i++) {
- const point1 = latlngs[i];
- const point2 = latlngs[i + 1];
-
- const distance = point1.distanceTo(point2);
-
- totalDistance += distance;
- }
-
- if (!isLine) {
- latlngs.pop(latlngs[latlngs.length - 1]);
- }
-
- const formatPerimeter = (perimeter) => {
- if (perimeter >= 1000) {
- return `${(perimeter / 1000).toFixed(2)} km`;
- } else {
- return `${perimeter.toFixed(2)} m`;
- }
- };
-
- return formatPerimeter(totalDistance);
- },
-
- _UpdateDistanceByLatLngs: function (latlngs) {
- let totalDistance = 0;
-
- for (let i = 0; i < latlngs.length - 1; i++) {
- const point1 = L.latLng(latlngs[i][0], latlngs[i][1]);
- const point2 = L.latLng(latlngs[i + 1][0], latlngs[i + 1][1]);
-
- const distance = point1.distanceTo(point2);
- totalDistance += distance;
- }
-
- const formatPerimeter = (perimeter) => {
- if (perimeter >= 1000) {
- return `${(perimeter / 1000).toFixed(2)} km`;
- } else {
- return `${perimeter.toFixed(2)} m`;
- }
- };
-
- return formatPerimeter(totalDistance);
- },
-
- calculateDistance: function (latlngs) {
- let totalDistance = 0;
-
- for (let i = 0; i < latlngs.length - 1; i++) {
- const point1 = latlngs[i];
- const point2 = latlngs[i + 1];
-
- const distance = point1.distanceTo(point2);
-
- totalDistance += distance;
- }
-
- return totalDistance;
- },
-
- calculateArea: function (latlngs) {
- const toRadians = (degree) => (degree * Math.PI) / 180;
-
- if (latlngs.length < 3) return 0;
-
- const earthRadius = 6378137;
-
- let total = 0;
- for (let i = 0, l = latlngs.length; i < l; i++) {
- const [lat1, lon1] = latlngs[i];
- const [lat2, lon2] = latlngs[(i + 1) % l];
-
- total +=
- toRadians(lon2 - lon1) *
- (2 + Math.sin(toRadians(lat1)) + Math.sin(toRadians(lat2)));
- }
-
- total = Math.abs((total * earthRadius * earthRadius) / 2);
-
- const formatArea = (area) => {
- if (area >= 1000000) {
- return `${(area / 1000000).toFixed(2)} km²`;
- } else {
- return `${area.toFixed(2)} m²`;
- }
- };
-
- return formatArea(total);
- },
-
- formatDistance: function (perimeter) {
- const formatPerimeter = (perimeter) => {
- if (perimeter >= 1000) {
- return `${(perimeter / 1000).toFixed(2)} km`;
- } else {
- return `${perimeter.toFixed(2)} m`;
- }
- };
-
- return formatPerimeter(perimeter);
- },
-
- _toggleMeasure: function (btnId = "", activeIcon = "", inactiveIcon = "") {
- if (this.options.checkonedrawpoligon) {
- this.options.checkonedrawpoligon = false;
- } else {
- this._measureHandler.enable();
- }
- },
-
- _clearMeasurements: function () {
- this._measureLayers.clearLayers();
- },
-
- changeColorByActivePolyline: function (map, customID) {
- this._measureLayers.eachLayer(function (layer) {
- const polyline = layer;
- if (layer instanceof L.Polyline) {
- if (layer.customID === customID) {
- polyline._path.classList.remove("custom-polyline");
- polyline.enableEdit();
- } else {
- polyline._path.classList.add("custom-polyline");
- polyline.disableEdit();
- }
- }
- });
- },
-
- changeColorByLastShape: function (map) {
- let lastPolyline = null;
-
- this._measureLayers.eachLayer(function (layer) {
- if (layer instanceof L.Polyline) {
- lastPolyline = layer;
- layer._path.classList.add("custom-polyline");
- }
- });
-
- if (lastPolyline) {
- lastPolyline._path.classList.remove("custom-polyline");
- }
- },
-
- getVisiblePolylines: function (map) {
- const visiblePolylines = [];
- const mapBounds = map.getBounds();
-
- this._measureLayers.eachLayer(function (layer) {
- if (layer instanceof L.Polyline) {
- if (mapBounds.intersects(layer.getBounds())) {
- visiblePolylines.push(layer);
- }
- }
- });
-
- return visiblePolylines;
- },
-
- getVisiblePolylinesIds: function (polylinesArr) {
- const idsPolylinesArr = [];
- this.options.visiblePolylines = [];
- polylinesArr.forEach((m) => {
- idsPolylinesArr.push(m.customID);
- this.options.visiblePolylines.push(m.customID);
- });
-
- this.options.cbVisiblePolylinesChange(idsPolylinesArr);
- },
-
- getAllPolylines: function (map) {
- const polylines = [];
-
- this._measureLayers.eachLayer(function (layer) {
- if (layer instanceof L.Polyline) {
- polylines.push(layer);
- }
- });
-
- return polylines;
- },
-
- removePolylineById: function (map, customID) {
- const self = this;
- this._measureLayers.eachLayer(function (layer) {
- if (layer instanceof L.Polyline && layer.customID === customID) {
- self._measureLayers.removeLayer(layer);
- }
- });
- },
-
- showActiveShape: function (map, coordinates) {
- this.options.moveToShape = true;
- const bounds = L.latLngBounds(coordinates);
- map.fitBounds(bounds);
- },
-
- fitMapToPolylines: function (map, polylines) {
- if (polylines.length === 0) {
- return;
- }
-
- const allBounds = L.latLngBounds();
-
- polylines.forEach((polyline) => {
- const polylineBounds = polyline.getBounds();
- allBounds.extend(polylineBounds);
- });
-
- map.fitBounds(allBounds);
- },
-
- replaceLineToPolygon: function (map, layer) {
- const latlngsJSON = layer.toGeoJSON();
- const prepeareCoordinates = latlngsJSON.geometry.coordinates.map((l) => {
- return l.reverse();
- });
-
- map.removeLayer(layer);
-
- prepeareCoordinates.push(prepeareCoordinates[0]);
-
- const options = {
- color: "#267bdcd4",
- fillColor: "#267bdcd4",
- opacity: 1,
- weigt: 3,
- };
- const distance = this._UpdateDistanceByLatLngs(prepeareCoordinates);
- const square = this.calculateArea(prepeareCoordinates);
- const preparePolygon = {
- coordinates: prepeareCoordinates,
- options,
- shapeId: layer.customID,
- distance: distance,
- number: this.options.measurementOrder,
- area: square,
- shapeType: this.options.shapeMode,
- };
-
- const polygon = L.polygon(prepeareCoordinates, options);
-
- polygon.customID = layer.customID;
- polygon.customShape = "polygon";
-
- polygon.addTo(this._measureLayers).showMeasurements().enableEdit();
- // polygon.on("dblclick", this._onPolygonClick.bind(this, map));
- polygon.on("click", () => {
- this.options.cbSetActiveShape(polygon.customID);
- this.options.cbSetUpdateStatusHandler(false);
- this.options.checkonedrawpoligon = false;
- });
- polygon.on(
- "editable:drag editable:dragstart editable:dragend editable:vertex:drag editable:vertex:deleted",
- this._onPolylineDrag.bind(this)
- );
-
- polygon.on("editable:vertex:dragend", () => {
- this.options.cbSetUpdateStatusHandler(false);
- // Reset status to WAITING when vertex editing ends
- if (this.options.cbSetMapStatus) {
- this.options.cbSetMapStatus("WAITING");
- }
- });
-
- // Reset status to WAITING when drag ends
- polygon.on("editable:dragend", () => {
- if (this.options.cbSetMapStatus) {
- this.options.cbSetMapStatus("WAITING");
- }
- });
-
- this.options.polygonMode = false;
-
- this.options.checkonedrawpoligon = true;
-
- this._toggleMeasure(
- "img_plg_lines",
- "icon_lineActive",
- "icon_lineInactive"
- );
-
- // this.options.checkonedrawpoligon = false;
-
- // this._measureHandler.disable();
-
- return preparePolygon;
- },
- getVisibleShapeIdsArr: function (map) {
- const allPolyLines = this.getVisiblePolylines(map);
- this.getVisiblePolylinesIds(allPolyLines);
- },
-
- findLastCreatedLayer: function (layerGroup) {
- let lastLayer = null;
- let highestId = -1;
-
- layerGroup.eachLayer((layer) => {
- if (layer._leaflet_id > highestId) {
- highestId = layer._leaflet_id;
- lastLayer = layer;
- }
- });
-
- return lastLayer;
- },
-
- loadMeasurements: function (map) {
- if (this.options.shapes.length !== 0) {
- this.options.shapes.forEach((shape) => {
- const { coordinates, options, shapeId, shapeType } = shape;
- const shapeName = shapeType === "line" ? "polyline" : "polygon";
-
- const savedShape = L[shapeName](coordinates, {
- showLength: true,
- className: "custom-polyline",
- shapeOptions: {
- weight: 4,
- color: "#267bdcd4",
- opacity: 1,
- },
- });
- savedShape.customID = shapeId;
- savedShape.addTo(this._measureLayers).showMeasurements().enableEdit();
- savedShape.on("click", () => {
- this.options.checkonedrawpoligon = true;
- this.options.cbSetActiveShape(savedShape.customID);
- this.options.cbSetUpdateStatusHandler(false);
- this.options.clickAfterShapeSelection = true;
- });
- savedShape.on("mouseout", (e) => {
- // this.options.checkonedrawpoligon = false;
- });
- savedShape.on("mouseover", (e) => {
- if (this.options.customTooltip) {
- this.options.customTooltip.style.visibility = "hidden";
- }
- });
- savedShape.on(
- "editable:drag editable:dragstart editable:dragend editable:vertex:drag editable:vertex:deleted",
- this._onPolylineDrag.bind(this)
- );
-
- savedShape.on("editable:vertex:dragend", () => {
- this.options.cbSetUpdateStatusHandler(false);
- // Reset status to WAITING when vertex editing ends
- if (this.options.cbSetMapStatus) {
- this.options.cbSetMapStatus("WAITING");
- }
- });
-
- // Reset status to WAITING when drag ends
- savedShape.on("editable:dragend", () => {
- if (this.options.cbSetMapStatus) {
- this.options.cbSetMapStatus("WAITING");
- }
- });
- });
- }
- },
-
- _toggleMeasurementBtn: function () {
- if (this.options.changeModeButtonActive) {
- document.getElementById("img_plg_lines").src =
- this.options.icon_lineInactive;
- this.options.changeModeButtonActive = false;
- } else {
- document.getElementById("img_plg_lines").src =
- this.options.icon_lineActive;
- this.options.changeModeButtonActive = true;
- }
- },
-
- toggleMeasurementMode: function (ifChangeMode = true, map) {
- const mode = this.options.measurementMode;
- if (mode === "measurement") {
- this._clearMeasurements();
- this.loadMeasurements();
- // const drawBtn = document.getElementById("draw_shape");
- // drawBtn.classList.remove("hide-draw-btn");
-
- document.getElementById("img_plg_lines").src =
- this.options.icon_lineActive;
-
- // if (this.options.isFirstLoading) {
- // this.options.isFirstLoading = false;
- // }
-
- const customTooltip = document.querySelector("#routedMap");
- customTooltip.style.cursor = "crosshair";
- } else {
- this._clearMeasurements();
-
- const customTooltip = document.querySelector("#routedMap");
- customTooltip.style.cursor = "pointer";
- if (this.options.currenLine) {
- this.options.currenLine.disable();
- }
- customTooltip.style.cursor = "pointer";
- // const drawBtn = document.getElementById("draw_shape");
- // drawBtn.classList.add("hide-draw-btn");
- this.options.checkonedrawpoligon = false;
- document.getElementById("img_plg_lines").src =
- this.options.icon_lineInactive;
- }
-
- if (ifChangeMode) {
- this.options.cbToggleMeasurementMode();
- }
- },
-
- changeMeasurementMode: function (mode, map) {
- this.options.measurementMode = mode;
- this.toggleMeasurementMode(false, map);
- },
- changeMeasurementsArr: function (arr) {
- this.options.shapes = arr;
- },
- cancelDrawing: function () {
- if (this.options.ifDrawing) {
- this._measureHandler.disable();
- this.options.ifDrawing = false;
-
- this._measureLayers.clearLayers();
-
- this.options.cbSetDrawingStatus(false);
- this.options.cbDeleteVisibleShapeById(5555);
- // this.options.checkonedrawpoligon = true;
- }
- },
-});
-
-// Adds the method to create a new instance of the control
-L.control.measurePolygon = function (options) {
- return new L.Control.MeasurePolygon(options);
-};
diff --git a/libraries/commons/measurements/src/lib/utils/measure.js b/libraries/commons/measurements/src/lib/utils/measure.ts
similarity index 58%
rename from libraries/commons/measurements/src/lib/utils/measure.js
rename to libraries/commons/measurements/src/lib/utils/measure.ts
index cfb781e46b..036c8e08dd 100644
--- a/libraries/commons/measurements/src/lib/utils/measure.js
+++ b/libraries/commons/measurements/src/lib/utils/measure.ts
@@ -1,12 +1,21 @@
-!(function () {
+import L from "leaflet";
+
+(function () {
"use strict";
- L.Marker.Measurement = L[L.Layer ? "Layer" : "Class"].extend({
+ L.Marker.Measurement = L.Layer.extend({
options: {
pane: "markerPane",
},
- initialize: function (latlng, measurement, title, rotation, options) {
+ initialize: function (
+ this: L.Marker.Measurement,
+ latlng: L.LatLng,
+ measurement: string,
+ title: string,
+ rotation: number,
+ options?: L.Marker.MeasurementOptions
+ ) {
L.setOptions(this, options);
this._latlng = latlng;
@@ -15,36 +24,42 @@
this._rotation = rotation;
},
- addTo: function (map) {
+ addTo: function (this: L.Marker.Measurement, map: L.Map) {
map.addLayer(this);
return this;
},
- onAdd: function (map) {
+ onAdd: function (this: L.Marker.Measurement, map: L.Map) {
this._map = map;
- var pane = this.getPane ? this.getPane() : map.getPanes().markerPane;
- var el = (this._element = L.DomUtil.create(
+ const pane = this.getPane
+ ? this.getPane()
+ : (map as any).getPanes().markerPane;
+ const el = (this._element = L.DomUtil.create(
"div",
"leaflet-zoom-animated leaflet-measure-path-measurement",
pane
));
- var inner = L.DomUtil.create("div", "", el);
+ const inner = L.DomUtil.create("div", "", el);
inner.title = this._title;
inner.innerHTML = this._measurement;
map.on("zoomanim", this._animateZoom, this);
this._setPosition();
+ return this;
},
- onRemove: function (map) {
+ onRemove: function (this: L.Marker.Measurement, map: L.Map) {
map.off("zoomanim", this._animateZoom, this);
- var pane = this.getPane ? this.getPane() : map.getPanes().markerPane;
+ const pane = this.getPane
+ ? this.getPane()
+ : (map as any).getPanes().markerPane;
pane.removeChild(this._element);
this._map = null;
+ return this;
},
- _setPosition: function () {
+ _setPosition: function (this: L.Marker.Measurement) {
L.DomUtil.setPosition(
this._element,
this._map.latLngToLayerPoint(this._latlng)
@@ -52,8 +67,11 @@
this._element.style.transform += " rotate(" + this._rotation + "rad)";
},
- _animateZoom: function (opt) {
- var pos = this._map
+ _animateZoom: function (
+ this: L.Marker.Measurement,
+ opt: { zoom: number; center: L.LatLng }
+ ) {
+ const pos = (this._map as any)
._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center)
.round();
L.DomUtil.setPosition(this._element, pos);
@@ -62,12 +80,12 @@
});
L.marker.measurement = function (
- latLng,
- measurement,
- title,
- rotation,
- options
- ) {
+ latLng: L.LatLng,
+ measurement: string,
+ title: string,
+ rotation: number,
+ options?: L.Marker.MeasurementOptions
+ ): L.Marker.Measurement {
return new L.Marker.Measurement(
latLng,
measurement,
@@ -77,8 +95,9 @@
);
};
- var formatDistance = function (d) {
- var unit, feet;
+ const formatDistance = function (this: L.Polyline, d: number): string {
+ let unit: string;
+ let feet: number;
if (this._measurementOptions.imperial) {
feet = d / 0.3048;
@@ -105,8 +124,8 @@
}
};
- var formatArea = function (a) {
- var unit, sqfeet;
+ const formatArea = function (this: L.Polyline, a: number): string {
+ let unit: string;
if (this._measurementOptions.imperial) {
if (a > 404.685642) {
@@ -142,26 +161,26 @@
}
};
- var RADIUS = 6378137;
+ const RADIUS = 6378137;
// ringArea function copied from geojson-area
// (https://github.com/mapbox/geojson-area)
// This function is distributed under a separate license,
// see LICENSE.md.
- var ringArea = function ringArea(coords) {
- var rad = function rad(_) {
- return (_ * Math.PI) / 180;
+ const ringArea = function (coords: L.LatLng[]): number {
+ const rad = function (deg: number): number {
+ return (deg * Math.PI) / 180;
};
- var p1,
- p2,
- p3,
- lowerIndex,
- middleIndex,
- upperIndex,
- area = 0,
- coordsLength = coords.length;
+ let p1: L.LatLng,
+ p2: L.LatLng,
+ p3: L.LatLng,
+ lowerIndex: number,
+ middleIndex: number,
+ upperIndex: number;
+ let area = 0;
+ const coordsLength = coords.length;
if (coordsLength > 2) {
- for (var i = 0; i < coordsLength; i++) {
+ for (let i = 0; i < coordsLength; i++) {
if (i === coordsLength - 2) {
// i = N-2
lowerIndex = coordsLength - 2;
@@ -189,45 +208,54 @@
return Math.abs(area);
};
+
/**
* Handles the init hook for polylines and circles.
* Implements the showOnHover functionality if called for.
*/
- var addInitHook = function () {
- var showOnHover =
+ const addInitHook = function (this: L.Polyline) {
+ const showOnHover =
this.options.measurementOptions &&
this.options.measurementOptions.showOnHover;
if (this.options.showMeasurements && !showOnHover) {
this.showMeasurements();
}
if (this.options.showMeasurements && showOnHover) {
- this.on("mouseover", function () {
+ this.on("mouseover", function (this: L.Polyline) {
this.showMeasurements();
});
- this.on("mouseout", function () {
+ this.on("mouseout", function (this: L.Polyline) {
this.hideMeasurements();
});
}
};
- var override = function (method, fn, hookAfter) {
+ type MethodFunction = (...args: any[]) => any;
+
+ const override = function (
+ method: MethodFunction,
+ fn: (...args: any[]) => any,
+ hookAfter?: boolean
+ ): MethodFunction {
if (!hookAfter) {
- return function () {
- var originalReturnValue = method.apply(this, arguments);
- var args = Array.prototype.slice.call(arguments);
- args.push(originalReturnValue);
- return fn.apply(this, args);
+ return function (this: any, ...args: any[]) {
+ const originalReturnValue = method.apply(this, args);
+ const newArgs = [...args, originalReturnValue];
+ return fn.apply(this, newArgs);
};
} else {
- return function () {
- fn.apply(this, arguments);
- return method.apply(this, arguments);
+ return function (this: any, ...args: any[]) {
+ fn.apply(this, args);
+ return method.apply(this, args);
};
}
};
L.Polyline.include({
- showMeasurements: function (options) {
+ showMeasurements: function (
+ this: L.Polyline,
+ options?: L.Marker.MeasurementOptions
+ ) {
if (!this._map || this._measurementLayer) return this;
this._measurementOptions = L.extend(
@@ -254,7 +282,7 @@
return this;
},
- hideMeasurements: function () {
+ hideMeasurements: function (this: L.Polyline) {
if (!this._map) return this;
this._map.off("zoomend", this.updateMeasurements, this);
@@ -266,20 +294,23 @@
return this;
},
- onAdd: override(L.Polyline.prototype.onAdd, function (originalReturnValue) {
- var showOnHover =
- this.options.measurementOptions &&
- this.options.measurementOptions.showOnHover;
- if (this.options.showMeasurements && !showOnHover) {
- this.showMeasurements(this.options.measurementOptions);
- }
+ onAdd: override(
+ L.Polyline.prototype.onAdd,
+ function (this: L.Polyline, originalReturnValue: any) {
+ const showOnHover =
+ this.options.measurementOptions &&
+ this.options.measurementOptions.showOnHover;
+ if (this.options.showMeasurements && !showOnHover) {
+ this.showMeasurements(this.options.measurementOptions);
+ }
- return originalReturnValue;
- }),
+ return originalReturnValue;
+ }
+ ),
onRemove: override(
L.Polyline.prototype.onRemove,
- function (originalReturnValue) {
+ function (this: L.Polyline, originalReturnValue: any) {
this.hideMeasurements();
return originalReturnValue;
@@ -289,16 +320,7 @@
setLatLngs: override(
L.Polyline.prototype.setLatLngs,
- function (originalReturnValue) {
- this.updateMeasurements();
-
- return originalReturnValue;
- }
- ),
-
- spliceLatLngs: override(
- L.Polyline.prototype.spliceLatLngs,
- function (originalReturnValue) {
+ function (this: L.Polyline, originalReturnValue: any) {
this.updateMeasurements();
return originalReturnValue;
@@ -308,7 +330,7 @@
formatDistance: formatDistance,
formatArea: formatArea,
- getCentroid(points) {
+ getCentroid(this: L.Polyline, points: L.LatLng[]): L.LatLng {
let sumLat = 0;
let sumLng = 0;
const numPoints = points.length;
@@ -321,44 +343,53 @@
const centroidLat = sumLat / numPoints;
const centroidLng = sumLng / numPoints;
- return [centroidLat, centroidLng];
+ return L.latLng(centroidLat, centroidLng);
},
- updateMeasurements: function () {
+ updateMeasurements: function (this: L.Polyline) {
if (!this._measurementLayer) return this;
- var latLngs = this.getLatLngs(),
- isPolygon = this instanceof L.Polygon,
- options = this._measurementOptions,
- totalDist = 0,
- formatter,
- ll1,
- ll2,
- p1,
- p2,
- pixelDist,
- dist;
-
- if (latLngs && latLngs.length && L.Util.isArray(latLngs[0])) {
+ let latLngs = this.getLatLngs() as L.LatLng[] | L.LatLng[][];
+ const isPolygon = this instanceof L.Polygon;
+ const options = this._measurementOptions;
+ let totalDist = 0;
+ let formatter: (value: number) => string;
+ let ll1: L.LatLng,
+ ll2: L.LatLng,
+ p1: L.Point,
+ p2: L.Point,
+ pixelDist: number,
+ dist: number;
+
+ if (
+ latLngs &&
+ latLngs.length &&
+ Array.isArray(latLngs[0]) &&
+ (latLngs[0] as any).lat === undefined
+ ) {
// Outer ring is stored as an array in the first element,
// use that instead.
- latLngs = latLngs[0];
+ latLngs = (latLngs as L.LatLng[][])[0];
}
this._measurementLayer.clearLayers();
- if (this._measurementOptions.showDistances && latLngs.length > 1) {
+ if (
+ this._measurementOptions.showDistances &&
+ (latLngs as L.LatLng[]).length > 1
+ ) {
formatter =
this._measurementOptions.formatDistance ||
- L.bind(this.formatDistance, this);
-
- for (
- var i = 1, len = latLngs.length;
- (isPolygon && i <= len) || i < len;
- i++
- ) {
- ll1 = latLngs[i - 1];
- ll2 = latLngs[i % len];
+ (L.bind(this.formatDistance, this) as unknown as (
+ value: number
+ ) => string);
+
+ const latLngsArray = latLngs as L.LatLng[];
+ const len = latLngsArray.length;
+
+ for (let i = 1; (isPolygon && i <= len) || i < len; i++) {
+ ll1 = latLngsArray[i - 1];
+ ll2 = latLngsArray[i % len];
dist = ll1.distanceTo(ll2);
totalDist += dist;
@@ -379,7 +410,7 @@
this._getRotation(ll1, ll2),
options
)
- .addTo(this._measurementLayer);
+ .addTo(this._measurementLayer as any);
}
}
@@ -393,36 +424,40 @@
0,
options
)
- .addTo(this._measurementLayer);
+ .addTo(this._measurementLayer as any);
}
}
- if (isPolygon && options.showArea && latLngs.length > 2) {
- formatter = options.formatArea || L.bind(this.formatArea, this);
- var area = ringArea(latLngs);
+ if (isPolygon && options.showArea && (latLngs as L.LatLng[]).length > 2) {
+ formatter =
+ options.formatArea ||
+ (L.bind(this.formatArea, this) as unknown as (
+ value: number
+ ) => string);
+ const area = ringArea(latLngs as L.LatLng[]);
L.marker
.measurement(
- this.getCentroid(latLngs),
+ this.getCentroid(latLngs as L.LatLng[]),
formatter(area),
options.lang.totalArea,
0,
options
)
- .addTo(this._measurementLayer);
+ .addTo(this._measurementLayer as any);
}
return this;
},
- _getRotation: function (ll1, ll2) {
- var p1 = this._map.project(ll1),
- p2 = this._map.project(ll2);
+ _getRotation: function (this: L.Polyline, ll1: L.LatLng, ll2: L.LatLng) {
+ const p1 = this._map.project(ll1);
+ const p2 = this._map.project(ll2);
return Math.atan((p2.y - p1.y) / (p2.x - p1.x));
},
});
- L.Polyline.addInitHook(function () {
+ L.Polyline.addInitHook(function (this: L.Polyline) {
addInitHook.call(this);
});
})();
diff --git a/libraries/commons/measurements/src/lib/utils/measurement-geometry.ts b/libraries/commons/measurements/src/lib/utils/measurement-geometry.ts
new file mode 100644
index 0000000000..35316b11cf
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/utils/measurement-geometry.ts
@@ -0,0 +1,128 @@
+/**
+ * Measurement Geometry Utilities
+ * Pure calculation functions for distance and area measurements
+ * Extracted from measure-path.ts to reduce file size and improve testability
+ */
+
+import L from "leaflet";
+
+/**
+ * Calculate total distance along a path of LatLng points
+ * @param latlngs - Array of Leaflet LatLng points
+ * @returns Distance in meters
+ */
+export function calculateDistance(latlngs: L.LatLng[]): number {
+ let totalDistance = 0;
+
+ for (let i = 0; i < latlngs.length - 1; i++) {
+ const point1 = latlngs[i];
+ const point2 = latlngs[i + 1];
+ const distance = point1.distanceTo(point2);
+ totalDistance += distance;
+ }
+
+ return totalDistance;
+}
+
+/**
+ * Calculate area of a polygon using spherical geometry
+ * @param latlngs - Array of [lat, lng] coordinate pairs forming a closed polygon
+ * @returns Formatted area string (e.g., "123.45 m²" or "1.23 km²")
+ */
+export function calculateArea(latlngs: number[][]): string {
+ const toRadians = (degree: number): number => (degree * Math.PI) / 180;
+
+ if (latlngs.length < 3) return "0 m²";
+
+ const earthRadius = 6378137; // meters
+
+ let total = 0;
+ for (let i = 0, l = latlngs.length; i < l; i++) {
+ const [lat1, lon1] = latlngs[i];
+ const [lat2, lon2] = latlngs[(i + 1) % l];
+
+ total +=
+ toRadians(lon2 - lon1) *
+ (2 + Math.sin(toRadians(lat1)) + Math.sin(toRadians(lat2)));
+ }
+
+ total = Math.abs((total * earthRadius * earthRadius) / 2);
+
+ return formatArea(total);
+}
+
+/**
+ * Format area value to human-readable string
+ * @param area - Area in square meters
+ * @returns Formatted string (e.g., "123.45 m²" or "1.23 km²")
+ */
+export function formatArea(area: number): string {
+ if (area >= 1000000) {
+ return `${(area / 1000000).toFixed(2)} km²`;
+ } else {
+ return `${area.toFixed(2)} m²`;
+ }
+}
+
+/**
+ * Format distance value to human-readable string
+ * @param distance - Distance in meters
+ * @returns Formatted string (e.g., "123.45 m" or "1.23 km")
+ */
+export function formatDistance(distance: number): string {
+ if (distance >= 1000) {
+ return `${(distance / 1000).toFixed(2)} km`;
+ } else {
+ return `${distance.toFixed(2)} m`;
+ }
+}
+
+/**
+ * Calculate and format distance for a Leaflet polyline/polygon layer
+ * @param layer - Leaflet Polyline or Polygon layer
+ * @returns Formatted distance string
+ */
+export function updateDistance(layer: L.Polyline | L.Polygon): string {
+ const isLine = layer.toGeoJSON().geometry.type === "LineString";
+ const latlngsRaw = isLine
+ ? layer.getLatLngs()
+ : (layer as L.Polygon).getLatLngs()[0];
+
+ // Type guard: ensure we have LatLng[]
+ let latlngs: L.LatLng[];
+ if (Array.isArray(latlngsRaw)) {
+ latlngs = latlngsRaw as L.LatLng[];
+ } else {
+ latlngs = [latlngsRaw as L.LatLng];
+ }
+
+ if (!isLine) {
+ latlngs.push(latlngs[0]); // Close polygon
+ }
+
+ const totalDistance = calculateDistance(latlngs);
+
+ if (!isLine) {
+ latlngs.pop(); // Remove duplicate closing point
+ }
+
+ return formatDistance(totalDistance);
+}
+
+/**
+ * Calculate and format distance from array of coordinate pairs
+ * @param coordinates - Array of [lat, lng] pairs
+ * @returns Formatted distance string
+ */
+export function updateDistanceByLatLngs(coordinates: number[][]): string {
+ let totalDistance = 0;
+
+ for (let i = 0; i < coordinates.length - 1; i++) {
+ const point1 = L.latLng(coordinates[i][0], coordinates[i][1]);
+ const point2 = L.latLng(coordinates[i + 1][0], coordinates[i + 1][1]);
+ const distance = point1.distanceTo(point2);
+ totalDistance += distance;
+ }
+
+ return formatDistance(totalDistance);
+}
diff --git a/libraries/commons/measurements/src/lib/utils/shapes.ts b/libraries/commons/measurements/src/lib/utils/shapes.ts
new file mode 100644
index 0000000000..877415b910
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/utils/shapes.ts
@@ -0,0 +1,20 @@
+/**
+ * Shape/measurement array utilities
+ */
+
+export function filterArrByIds(
+ arrIds: (string | number)[],
+ fullArray: any[]
+): any[] {
+ return fullArray.filter((item) => arrIds.includes(item.shapeId));
+}
+
+export function findLargestNumber(measurements: any[]): number {
+ let largestNumber = 0;
+ for (const item of measurements) {
+ if (item.number > largestNumber) {
+ largestNumber = item.number;
+ }
+ }
+ return largestNumber;
+}
diff --git a/libraries/commons/measurements/src/lib/utils/snapping.ts b/libraries/commons/measurements/src/lib/utils/snapping.ts
new file mode 100644
index 0000000000..7699603347
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/utils/snapping.ts
@@ -0,0 +1,308 @@
+/**
+ * Snapping utilities for measurement tools
+ */
+import L from "leaflet";
+import { latLng } from "@carma/leaflet";
+import { distanceMeters, metersPerPixel } from "@carma/geo/utils";
+
+import { SnappingPoint } from "../types";
+
+/** Snapping modifier key to temporarily disable snapping */
+export const SNAPPING_MODIFIER_KEY = "Alt";
+
+/** Default threshold for coordinate match (0.1 meters = 10cm) */
+export const EXACT_MATCH_METERS = 0.1;
+
+/**
+ * Screen pixel distance between two points
+ */
+export const screenPixelDistance = (
+ p1: { x: number; y: number },
+ p2: { x: number; y: number }
+): number => {
+ const dx = p1.x - p2.x;
+ const dy = p1.y - p2.y;
+ return Math.sqrt(dx * dx + dy * dy);
+};
+
+/**
+ * Convert pixel radius to meters at a given lat/zoom
+ */
+export const pixelRadiusToMeters = (
+ pixelRadius: number,
+ lat: number,
+ zoom: number
+): number => {
+ const mpp = metersPerPixel(zoom, lat as any);
+ return pixelRadius * mpp;
+};
+
+/**
+ * Distance in meters between two lat/lng positions (uses @carma/geo/utils)
+ */
+export const distanceBetweenLatLng = (
+ a: { lat: number; lng: number },
+ b: { lat: number; lng: number }
+): number =>
+ distanceMeters(
+ { latitude: a.lat, longitude: a.lng } as any,
+ { latitude: b.lat, longitude: b.lng } as any
+ );
+
+/**
+ * Check if a coordinate [lng, lat] matches a LatLng within threshold (default 0.1m)
+ */
+export const isCoordMatchLatLng = (
+ coord: [number, number],
+ latlng: { lat: number; lng: number },
+ thresholdMeters = EXACT_MATCH_METERS
+): boolean =>
+ distanceBetweenLatLng({ lat: coord[1], lng: coord[0] }, latlng) <
+ thresholdMeters;
+
+/**
+ * Create a snapping indicator marker
+ */
+export const createSnappingIndicator = (
+ latlng: { lat: number; lng: number },
+ map: L.Map
+): L.CircleMarker => {
+ return L.circleMarker([latlng.lat, latlng.lng], {
+ radius: 3.5,
+ color: "#000000",
+ fillColor: "#000000",
+ fillOpacity: 0.8,
+ weight: 1,
+ opacity: 0.8,
+ interactive: false,
+ }).addTo(map);
+};
+
+/**
+ * Find the closest snapping point within a pixel radius
+ */
+export const findClosestSnappingPoint = (
+ points: SnappingPoint[],
+ referencePoint: { x: number; y: number },
+ maxPixelRadius: number,
+ projectToScreen: (coord: [number, number]) => { x: number; y: number }
+): { point: SnappingPoint; distance: number } | null => {
+ let closest: { point: SnappingPoint; distance: number } | null = null;
+
+ for (const snappingPoint of points) {
+ const screenPoint = projectToScreen(snappingPoint.coordinates);
+ const dist = screenPixelDistance(screenPoint, referencePoint);
+
+ if (dist <= maxPixelRadius && (!closest || dist < closest.distance)) {
+ closest = { point: snappingPoint, distance: dist };
+ }
+ }
+
+ return closest;
+};
+
+/**
+ * Get the first vertex of a draw handler's polygon if it has 3+ vertices
+ */
+export const getFirstVertexIfClosable = (drawHandler: any): L.LatLng | null => {
+ if (drawHandler?._poly?._latlngs?.length >= 3) {
+ return drawHandler._poly._latlngs[0];
+ }
+ return null;
+};
+
+/**
+ * Get the last vertex of a draw handler's line if it has 2+ vertices
+ */
+export const getLastVertexIfFinishable = (
+ drawHandler: any
+): L.LatLng | null => {
+ const latlngs = drawHandler?._poly?._latlngs;
+ if (latlngs?.length >= 2) {
+ return latlngs[latlngs.length - 1];
+ }
+ return null;
+};
+
+/**
+ * Check if a position matches the first vertex of a closable polygon
+ */
+export const isFirstVertexMatch = (
+ drawHandler: any,
+ position: { lat: number; lng: number },
+ thresholdMeters = EXACT_MATCH_METERS
+): boolean => {
+ const firstVertex = getFirstVertexIfClosable(drawHandler);
+ return firstVertex
+ ? distanceBetweenLatLng(position, firstVertex) < thresholdMeters
+ : false;
+};
+
+/**
+ * Check if a position matches the last vertex of a finishable line
+ */
+export const isLastVertexMatch = (
+ drawHandler: any,
+ position: { lat: number; lng: number },
+ thresholdMeters = EXACT_MATCH_METERS
+): boolean => {
+ const lastVertex = getLastVertexIfFinishable(drawHandler);
+ return lastVertex
+ ? distanceBetweenLatLng(position, lastVertex) < thresholdMeters
+ : false;
+};
+
+/**
+ * Try to close a polygon by calling _finishShape directly
+ */
+export const tryClosePolygon = (drawHandler: any): boolean => {
+ const firstVertex = getFirstVertexIfClosable(drawHandler);
+ if (!firstVertex) return false;
+
+ if (drawHandler._finishShape) {
+ // If the handler is a Polyline handler (but not a Polygon handler),
+ // it doesn't automatically close the shape. We must manually add the closing vertex.
+ if (drawHandler.type === "polyline") {
+ console.debug(
+ "[snapping] Closing polyline to form polygon: adding first vertex"
+ );
+ drawHandler.addVertex(firstVertex);
+ }
+
+ console.debug("[snapping] Closing polygon via _finishShape");
+ drawHandler._finishShape();
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Try to finish a line measurement by calling _finishShape directly
+ */
+export const tryFinishLine = (drawHandler: any): boolean => {
+ if (!drawHandler?._finishShape) return false;
+
+ const latlngs = drawHandler?._poly?._latlngs;
+ if (!latlngs || latlngs.length < 2) return false;
+
+ console.debug("[snapping] Finishing line via _finishShape");
+ drawHandler._finishShape();
+ return true;
+};
+
+/**
+ * Prepare a Leaflet LatLng from a GeoJSON Point-like feature
+ */
+export const toLatLngFromClosestPoint = (closestPoint: any) => {
+ if (
+ !closestPoint ||
+ !closestPoint.geometry ||
+ !closestPoint.geometry.coordinates
+ ) {
+ return null;
+ }
+ const [lng, lat] = closestPoint.geometry.coordinates;
+ return latLng(lat, lng);
+};
+
+/**
+ * Adjust click position for snapping (fires synthetic event)
+ */
+export const adjustClickPosition = (
+ domEvent: MouseEvent,
+ closestPoint: any,
+ eventType: string,
+ leafletMap: any,
+ currentDrawHandler?: any
+) => {
+ const containerPoint = leafletMap.mouseEventToContainerPoint(domEvent);
+ const shiftedContainerPoint = L.point(containerPoint.x, containerPoint.y);
+
+ if (!closestPoint) {
+ return false;
+ }
+
+ const [lng, lat] = closestPoint.geometry.coordinates;
+ const finalLatLng = latLng(lat, lng);
+
+ // Check if we're drawing and snapped to first vertex (polygon closure)
+ if (isFirstVertexMatch(currentDrawHandler, finalLatLng)) {
+ if (tryClosePolygon(currentDrawHandler)) {
+ return true;
+ }
+ }
+
+ console.log("[snapping] Firing synthetic Leaflet event", {
+ eventType,
+ latlng: finalLatLng,
+ originalCoords: [domEvent.clientX, domEvent.clientY],
+ });
+
+ leafletMap.fire(eventType, {
+ latlng: finalLatLng,
+ containerPoint: shiftedContainerPoint,
+ originalEvent: domEvent,
+ _isSyntheticSnap: true,
+ });
+
+ return true;
+};
+
+/**
+ * Handle potential duplicate vertex (snapped or unsnapped).
+ * Returns true if the vertex was handled (either finished the line or ignored as duplicate).
+ * Returns false if the vertex should be added as new.
+ */
+export const handleDuplicateVertex = (
+ drawHandler: any,
+ position: { lat: number; lng: number },
+ thresholdMeters: number
+): boolean => {
+ if (drawHandler._markers && drawHandler._markers.length > 0) {
+ const lastMarker = drawHandler._markers[drawHandler._markers.length - 1];
+ const lastLatLng = lastMarker.getLatLng();
+
+ if (distanceBetweenLatLng(position, lastLatLng) < thresholdMeters) {
+ // It is the same point as the last one.
+ // Try to finish if possible (Leaflet Draw logic: click last point to finish)
+ if (tryFinishLine(drawHandler)) {
+ return true; // Handled (finished)
+ }
+ // If we couldn't finish (e.g. only 1 point), it's just a duplicate. Ignore it.
+ console.debug(
+ "[snapping] Ignoring duplicate vertex click (0-length segment prevention)"
+ );
+ return true; // Handled (ignored)
+ }
+ }
+ return false; // Not a duplicate
+};
+
+/**
+ * Create a GeoJSON Feature for the snapping candidate (snapped or unsnapped)
+ */
+export const createSnappingFeature = (
+ closestResult: { point: SnappingPoint; distance: number } | null,
+ mouseLatLng: { lat: number; lng: number }
+): any => {
+ const isSnapped = !!closestResult;
+ const coordinates = closestResult
+ ? closestResult.point.coordinates
+ : [mouseLatLng.lng, mouseLatLng.lat];
+
+ const sourceId = closestResult
+ ? closestResult.point.sourceId
+ : "pointerposition";
+
+ return {
+ type: "Feature",
+ geometry: {
+ type: "Point",
+ coordinates: coordinates,
+ },
+ properties: {
+ isSnapped: isSnapped,
+ source: sourceId,
+ },
+ };
+};
diff --git a/libraries/commons/measurements/src/lib/utils/storage.ts b/libraries/commons/measurements/src/lib/utils/storage.ts
new file mode 100644
index 0000000000..d6834faf18
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/utils/storage.ts
@@ -0,0 +1,33 @@
+/**
+ * LocalForage storage utilities
+ */
+import localforage from "localforage";
+
+export const setFromLocalforage = async (
+ lfKey: string,
+ setter: (value: any) => void,
+ fallbackValue?: any,
+ forceFallback?: boolean
+) => {
+ try {
+ const value = await localforage.getItem(lfKey);
+ if (value !== undefined && value !== null) {
+ setter(value);
+ } else if (fallbackValue !== undefined || forceFallback === true) {
+ setter(fallbackValue);
+ }
+ } catch (error) {
+ console.warn(`Failed to load ${lfKey} from localStorage:`, error);
+ if (fallbackValue !== undefined || forceFallback === true) {
+ setter(fallbackValue);
+ }
+ }
+};
+
+export const saveToLocalforage = async (lfKey: string, value: any) => {
+ try {
+ await localforage.setItem(lfKey, value);
+ } catch (error) {
+ console.warn(`Failed to save ${lfKey} to localStorage:`, error);
+ }
+};
diff --git a/libraries/commons/measurements/src/lib/utils/vertex-click-handler.ts b/libraries/commons/measurements/src/lib/utils/vertex-click-handler.ts
new file mode 100644
index 0000000000..35ca4d47db
--- /dev/null
+++ b/libraries/commons/measurements/src/lib/utils/vertex-click-handler.ts
@@ -0,0 +1,159 @@
+import type { LeafletMouseEvent } from "@carma/leaflet";
+import type { Layer, Control, Marker } from "leaflet";
+
+/**
+ * Handler for vertex clicks during measurement drawing
+ * Decides action based on which vertex was clicked:
+ * - First vertex (with 3+ total): Close polygon
+ * - Last vertex: Finish line
+ * - Middle vertices: No action
+ */
+export function createVertexClickHandler(
+ getMeasureHandler: () =>
+ | (Control.DrawHandler & {
+ _markers?: Marker[];
+ _finishShape?: () => void;
+ _poly?: { _latlngs?: unknown[] };
+ })
+ | null
+ | undefined,
+ options: Pick,
+ getCurrentVertexCount: () => number,
+ map?: any,
+ getIsFinishingShape?: () => boolean,
+ setIsFinishingShape?: (value: boolean) => void,
+ getLastVertexTimestamp?: () => number
+) {
+ const handler = function (e: LeafletMouseEvent) {
+ const measureHandler = getMeasureHandler();
+ if (!measureHandler) {
+ return; // No measure handler, ignore
+ }
+
+ // CRITICAL: Stop propagation immediately to prevent the map from receiving this click
+ // This prevents Leaflet.Draw's map click handler from adding a new vertex when we click an existing one
+ if (e.originalEvent) {
+ e.originalEvent.stopPropagation();
+ e.originalEvent.preventDefault();
+ }
+ // Since we are now attaching this handler directly to the marker,
+ // we can get the marker from e.target instead of searching for it.
+ const clickedMarker = e.target as any;
+ const clickedHandle = clickedMarker.customHandle;
+
+ if (clickedHandle === undefined || clickedHandle === null) {
+ console.warn(
+ "[measure-path] Clicked marker has no handle index",
+ clickedMarker
+ );
+ return;
+ }
+
+ const markers = measureHandler._markers;
+ if (!markers) return;
+
+ const vertexCount = markers.length;
+ const isFirst = clickedHandle === 0;
+ const isLast = clickedHandle === vertexCount - 1;
+
+ console.warn("[measure-path] Vertex clicked", {
+ handle: clickedHandle,
+ isFirst,
+ isLast,
+ totalVertices: vertexCount,
+ eventType: e.originalEvent?.type,
+ });
+
+ // CRITICAL: Ignore clicks on the last vertex if it was just added (ghost clicks / touch bounce)
+ if (isLast && getLastVertexTimestamp) {
+ const lastAdded = getLastVertexTimestamp();
+ const now = Date.now();
+ if (now - lastAdded < 200) {
+ console.warn(
+ "[measure-path] Ignoring click on last vertex - too soon after addVertex",
+ { diff: now - lastAdded }
+ );
+ return;
+ }
+ }
+
+ // First vertex: close polygon (requires 3+ vertices and valid polyline data)
+ if (isFirst && vertexCount >= 3) {
+ const polyLatlngs = measureHandler._poly?._latlngs;
+ if (!polyLatlngs || polyLatlngs.length < 3) {
+ console.warn(
+ "[measure-path] Cannot close polygon - polyline data not ready",
+ {
+ polyLatlngs: polyLatlngs?.length,
+ }
+ );
+ return;
+ }
+ console.warn(
+ "[measure-path] Closing polygon - triggering finish like double-click"
+ );
+ options.shapeMode = "polygon";
+
+ // Set flag to prevent map click handler from starting new measurement
+ if (setIsFinishingShape) {
+ setIsFinishingShape(true);
+ }
+
+ // Trigger finish like double-click does
+ measureHandler._finishShape?.();
+
+ // Stop propagation to prevent map click handler from starting new measurement
+ e.originalEvent?.stopPropagation?.();
+ e.originalEvent?.preventDefault?.();
+
+ // Disable the handler after draw:created event fires
+ if (map) {
+ map.once("draw:created", () => {
+ if (measureHandler.disable) {
+ measureHandler.disable();
+ console.log(
+ "[measure-path] Disabled measurement handler after draw:created - ready for new measurement"
+ );
+ }
+ });
+ }
+ return;
+ }
+
+ // Last vertex: finish line
+ if (isLast) {
+ console.warn(
+ "[measure-path] Finishing line - will disable handler after completion"
+ );
+
+ // Set flag to prevent map click handler from starting new measurement
+ if (setIsFinishingShape) {
+ setIsFinishingShape(true);
+ }
+
+ measureHandler._finishShape?.();
+
+ // Stop propagation to prevent map click handler from starting new measurement
+ e.originalEvent?.stopPropagation?.();
+ e.originalEvent?.preventDefault?.();
+
+ // Disable the handler after draw:created event fires
+ if (map) {
+ map.once("draw:created", () => {
+ if (measureHandler.disable) {
+ measureHandler.disable();
+ console.log(
+ "[measure-path] Disabled measurement handler after draw:created - ready for new measurement"
+ );
+ }
+ setIsFinishingShape(false);
+ });
+ }
+ return;
+ }
+
+ console.log("[measure-path] Middle vertex - no action");
+ };
+
+ return handler;
+}
diff --git a/libraries/commons/measurements/tsconfig.json b/libraries/commons/measurements/tsconfig.json
index f474c15225..26e340899f 100644
--- a/libraries/commons/measurements/tsconfig.json
+++ b/libraries/commons/measurements/tsconfig.json
@@ -1,6 +1,5 @@
{
"extends": "../../../tsconfig.legacy.base.json",
- "files": [],
"include": [],
"references": [
{
diff --git a/libraries/commons/measurements/tsconfig.lib.json b/libraries/commons/measurements/tsconfig.lib.json
index 85dc7fbc79..61eeadcf4b 100644
--- a/libraries/commons/measurements/tsconfig.lib.json
+++ b/libraries/commons/measurements/tsconfig.lib.json
@@ -2,9 +2,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
- "declaration": true,
- "types": ["node"]
+ "declaration": true
},
- "include": ["src/**/*.ts", "src/lib/lib-measurements.tsx"],
- "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js"],
+ "exclude": ["src/**/*.spec.ts"]
}
diff --git a/libraries/commons/measurements/vite.config.ts b/libraries/commons/measurements/vite.config.ts
new file mode 100644
index 0000000000..e3ee109f12
--- /dev/null
+++ b/libraries/commons/measurements/vite.config.ts
@@ -0,0 +1,37 @@
+///
+import { defineConfig } from 'vite';
+import dts from 'vite-plugin-dts';
+import * as path from 'path';
+import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
+
+export default defineConfig({
+ root: __dirname,
+ cacheDir: '../../../../node_modules/.vite/libraries/commons/measurements',
+
+ plugins: [
+ nxViteTsPaths(),
+ nxCopyAssetsPlugin(['*.md']),
+ dts({
+ entryRoot: 'src',
+ tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
+ }),
+ ],
+
+ build: {
+ outDir: '../../../../dist/libraries/commons/measurements',
+ reportCompressedSize: true,
+ commonjsOptions: {
+ transformMixedEsModules: true,
+ },
+ lib: {
+ entry: 'src/index.ts',
+ name: 'measurements',
+ fileName: 'index',
+ formats: ['es'],
+ },
+ rollupOptions: {
+ external: ['leaflet', 'react', 'react-dom', 'react-leaflet', 'react-cismap'],
+ },
+ },
+});
diff --git a/libraries/mapping/engines/leaflet/src/index.ts b/libraries/mapping/engines/leaflet/src/index.ts
index 6c806623ab..7af111b32b 100644
--- a/libraries/mapping/engines/leaflet/src/index.ts
+++ b/libraries/mapping/engines/leaflet/src/index.ts
@@ -1,3 +1,44 @@
+import L from "leaflet";
+
export { LeafletMapStateChangeEvents } from "./lib/events";
export * from "./lib/LatLng";
export * from "./lib/Map";
+
+// Re-exports from Leaflet
+export const Browser = L.Browser;
+
+export const Control = L.Control;
+export type Control = L.Control;
+export type ControlOptions = L.ControlOptions;
+
+export const DomEvent = L.DomEvent;
+export const DomUtil = L.DomUtil;
+
+export const latLngBounds = L.latLngBounds;
+export const LatLngBounds = L.LatLngBounds;
+export type LatLngBounds = L.LatLngBounds;
+
+export const Layer = L.Layer;
+export type Layer = L.Layer;
+
+export const layerGroup = L.layerGroup;
+export const LayerGroup = L.LayerGroup;
+export type LayerGroup = L.LayerGroup;
+
+export const LeafletControl = L.Control;
+export type LeafletControl = L.Control;
+
+export type LeafletEvent = L.LeafletEvent;
+export type LeafletMouseEvent = L.LeafletMouseEvent;
+
+export const point = L.point;
+export const Point = L.Point;
+export type Point = L.Point;
+
+export const polygon = L.polygon;
+export const Polygon = L.Polygon;
+export type Polygon = L.Polygon;
+
+export const polyline = L.polyline;
+export const Polyline = L.Polyline;
+export type Polyline = L.Polyline;
diff --git a/libraries/mapping/engines/leaflet/src/lib/LatLng.ts b/libraries/mapping/engines/leaflet/src/lib/LatLng.ts
index cb1c6dcf49..ae83578d3b 100644
--- a/libraries/mapping/engines/leaflet/src/lib/LatLng.ts
+++ b/libraries/mapping/engines/leaflet/src/lib/LatLng.ts
@@ -1,6 +1,11 @@
-import { LatLng as LeafletLatLng } from "leaflet";
+import L from "leaflet";
import type { Degrees } from "@carma/units/types";
+export const LatLng = L.LatLng;
+export type LatLng = L.LatLng;
+
+export const latLng = L.latLng;
+
export type LatLngJson = {
latitude: Degrees;
longitude: Degrees;
@@ -9,9 +14,7 @@ export type LatLngJson = {
/**
* Convert Leaflet LatLng to CARMA LatLng.deg
*/
-export const leafletLatLngToLatLngJson = (
- latLng: LeafletLatLng
-): LatLngJson => {
+export const leafletLatLngToLatLngJson = (latLng: LatLng): LatLngJson => {
return {
latitude: latLng.lat as Degrees,
longitude: latLng.lng as Degrees,
@@ -21,8 +24,6 @@ export const leafletLatLngToLatLngJson = (
/**
* Convert CARMA LatLng.deg to Leaflet LatLng tuple
*/
-export const latLngUnitsTypedToLatLngJson = (
- latLng: LatLngJson
-): LeafletLatLng => {
- return new LeafletLatLng(latLng.latitude, latLng.longitude);
+export const latLngUnitsTypedToLatLngJson = (latLng: LatLngJson): LatLng => {
+ return new LatLng(latLng.latitude, latLng.longitude);
};
diff --git a/libraries/mapping/engines/leaflet/src/lib/Map.ts b/libraries/mapping/engines/leaflet/src/lib/Map.ts
index 2e795197eb..d01d166704 100644
--- a/libraries/mapping/engines/leaflet/src/lib/Map.ts
+++ b/libraries/mapping/engines/leaflet/src/lib/Map.ts
@@ -1,15 +1,16 @@
// prevent namespace collisions with other mapping libraries and JavaScript built-in Map type
-import { Map as LeafletMap, LatLng } from "leaflet";
+import L from "leaflet";
-export { LeafletMap };
+export const LeafletMap = L.Map;
+export type LeafletMap = L.Map;
export type LeafletView = {
- center: LatLng;
+ center: L.LatLng;
zoom: number;
};
export const isLeafletMap = (map: unknown): map is LeafletMap => {
- return map instanceof LeafletMap;
+ return map instanceof L.Map;
};
export const getLeafletView = (leaflet: LeafletMap): LeafletView => {
diff --git a/libraries/mapping/utils/src/lib/hooks/useLeafletZoomControls.ts b/libraries/mapping/utils/src/lib/hooks/useLeafletZoomControls.ts
index da9f9a2bce..7c7bf2c77d 100644
--- a/libraries/mapping/utils/src/lib/hooks/useLeafletZoomControls.ts
+++ b/libraries/mapping/utils/src/lib/hooks/useLeafletZoomControls.ts
@@ -1,5 +1,5 @@
import { useCallback, useContext } from "react";
-import type { Map as LeafletMap } from "leaflet";
+import { LeafletMap } from "@carma/leaflet";
import { TopicMapContext } from "react-cismap/contexts/TopicMapContextProvider";
import { logOnce } from "@carma-commons/utils";
diff --git a/libraries/mapping/utils/tsconfig.json b/libraries/mapping/utils/tsconfig.json
index 3ceab515a4..ec5af9f447 100644
--- a/libraries/mapping/utils/tsconfig.json
+++ b/libraries/mapping/utils/tsconfig.json
@@ -1,8 +1,7 @@
{
// Using legacy for now - contains old code
"extends": "../../../tsconfig.legacy.base.json",
- "compilerOptions": {},
- "files": [],
+ "includes": [],
"references": [
{
"path": "./tsconfig.lib.json"
diff --git a/libraries/mapping/utils/tsconfig.lib.json b/libraries/mapping/utils/tsconfig.lib.json
index 56cd2a7f98..b744da8324 100644
--- a/libraries/mapping/utils/tsconfig.lib.json
+++ b/libraries/mapping/utils/tsconfig.lib.json
@@ -3,7 +3,6 @@
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
- "types": ["node", "vite/client"]
},
"include": ["src/**/*.ts"],
"exclude": ["vite.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
diff --git a/libraries/mapping/utils/tsconfig.spec.json b/libraries/mapping/utils/tsconfig.spec.json
index 05a0e18393..bcf47646bd 100644
--- a/libraries/mapping/utils/tsconfig.spec.json
+++ b/libraries/mapping/utils/tsconfig.spec.json
@@ -6,21 +6,14 @@
"vitest/globals",
"vitest/importMeta",
"vite/client",
- "node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vitest.config.ts",
- "src/**/*.test.ts",
"src/**/*.spec.ts",
- "src/**/*.test.tsx",
"src/**/*.spec.tsx",
- "src/**/*.test.js",
- "src/**/*.spec.js",
- "src/**/*.test.jsx",
- "src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
diff --git a/libraries/types/src/index.d.ts b/libraries/types/src/index.d.ts
index 68d0ac4bfa..5c6d52a748 100644
--- a/libraries/types/src/index.d.ts
+++ b/libraries/types/src/index.d.ts
@@ -1,6 +1,5 @@
export * from "./lib/carma-config.d";
export * from "./lib/carma-layers.d";
-export * from "./lib/cesium-config.d";
export * from "./lib/cesium-shaders.d";
export * from "./lib/cismap-search.d";
export * from "./lib/feature-info.d";
diff --git a/libraries/types/src/lib/carma-config.d.ts b/libraries/types/src/lib/carma-config.d.ts
index bcb7715062..17e2473388 100644
--- a/libraries/types/src/lib/carma-config.d.ts
+++ b/libraries/types/src/lib/carma-config.d.ts
@@ -1 +1,5 @@
export type CarmaConfig = Record;
+
+export interface LayerCarmaConf {
+ skipSnapping?: boolean;
+}
diff --git a/libraries/types/src/lib/leaflet-extensions.d.ts b/libraries/types/src/lib/leaflet-extensions.d.ts
index 71617ed78a..413500152d 100644
--- a/libraries/types/src/lib/leaflet-extensions.d.ts
+++ b/libraries/types/src/lib/leaflet-extensions.d.ts
@@ -19,4 +19,278 @@ declare module "leaflet" {
options?: GeoJSONOptions
): LGeoJSON;
}
+
+ // Measurement plugin (leaflet-measure-path)
+ namespace Control {
+ interface MeasurementShapeData {
+ coordinates: number[][];
+ options: {
+ color: string;
+ fillColor: string | null;
+ opacity: number;
+ weight: number;
+ };
+ shapeId: number | string;
+ distance: string;
+ number: number;
+ area?: string | null;
+ shapeType: "line" | "polygon";
+ customTitle?: string;
+ }
+
+ interface DrawHandler {
+ _poly?: { _latlngs: LatLng[] };
+ _enabled?: boolean;
+ enable(): void;
+ disable(): void;
+ completeShape?: () => void;
+ addVertex?(latlng: LatLng): void;
+ }
+
+ class MeasurePolygon extends Control {
+ options: MeasurePolygonOptions;
+ _map: Map;
+ _measureLayers: LayerGroup;
+ _measureHandler: Draw.Polyline | Draw.Polygon | DrawHandler;
+ _lastOriginalClick: { latlng: LatLng; containerPoint: Point };
+
+ drawingPolygons(map: Map): void;
+ drawingLines(map: Map, event: LeafletMouseEvent): void;
+ onAdd(map: Map): HTMLElement;
+ _clearMeasurements(): void;
+ changeColorByActivePolyline(map: Map, customID: number | string): void;
+ changeColorByLastShape(map: Map): void;
+ showLastPolylineOnFirstLoding(map: Map): void;
+ getVisiblePolylines(map: Map): Polyline[];
+ getVisiblePolylinesIds(polylines: Polyline[]): void;
+ getAllPolylines(map: Map): Polyline[];
+ removePolylineById(map: Map, customID: number | string): void;
+ fitMapToAllPolylines(map: Map): void;
+ fitMapToPolylines(map: Map, polylines: Polyline[]): void;
+ convertPolylineToPolygon(map: Map, layer: Polyline): void;
+ loadMeasurements(map?: Map): void;
+ _toggleMeasurementBtn(): void;
+ toggleMeasurementMode(ifChangeMode?: boolean, map?: Map): void;
+ _UpdateDistance(layer: Polyline): string;
+ _toggleMeasure(
+ id: string,
+ iconActive: string,
+ inactiveIcon: string
+ ): void;
+ calculateArea(coordinates: number[][]): string;
+ calculateDistance(latlngs: LatLng[]): number;
+ formatDistance(distance: number): string;
+ saveShapeHandler(
+ layer: Polyline,
+ distance: string | null,
+ area: string | null,
+ map: Map
+ ): void;
+ _onPolylineDrag(event: LeafletEvent): void;
+ replaceLineToPolygon(map: Map, layer: Polyline): MeasurementShapeData;
+ getVisibleShapeIdsArr(map: Map): (number | string)[];
+ _UpdateDistanceByLatLngs(coordinates: number[][]): string;
+ showActiveShape(map: Map, coordinates: number[][]): void;
+ changeMeasurementMode(mode: string, map: Map): void;
+ changeMeasurementsArr(arr: MeasurementShapeData[]): void;
+ findLastCreatedLayer(layerGroup: LayerGroup): Layer | null;
+ cancelDrawing(): void;
+ startDrawing(): void;
+ _onPolygonClick(map: Map, event: LeafletMouseEvent): void;
+ _UpdateAreaperimeter(layer: Polygon): void;
+ }
+
+ interface MeasurePolygonOptions extends ControlOptions {
+ icon_lineActive: string;
+ icon_lineInactive: string;
+ icon_polygonActive: string;
+ icon_polygonInactive: string;
+ html_template: string;
+ height: number;
+ width: number;
+ mode_btn: string;
+ color_polygon: string;
+ fillColor_polygon: string;
+ weight_polygon: string;
+ isDrawing: boolean;
+ changeModeButtonActive: boolean;
+ msj_disable_tool: string;
+ shapes: MeasurementShapeData[];
+ activeShape: number | string | null;
+ shapeMode: "line" | "polygon";
+ measurementOrder: number;
+ moveToShape: boolean | MeasurementShapeData | null;
+ cb: () => void;
+ cbSaveShape: (shape: MeasurementShapeData) => void;
+ cdDeleteShape: (
+ id: number | string,
+ localShapeStore: MeasurementShapeData[]
+ ) => void;
+ cbUpdateShape: (
+ id: number | string,
+ newCoordinates: number[][],
+ newDistance: string,
+ newSquare: string | null
+ ) => void;
+ cbVisiblePolylinesChange: (ids: (number | string)[]) => void;
+ cbSetDrawingStatus: (status: boolean) => void;
+ cbSetDrawingShape: (shape: MeasurementShapeData | null) => void;
+ cbSetActiveShape: (id: number | string) => void;
+ cbSetUpdateStatusHandler: (status: boolean) => void;
+ cbMapMovingEndHandler: (status: boolean) => void;
+ cbSaveLastActiveShapeIdBeforeDrawingHandler: () => void;
+ cbChangeActiveCanceldShapeId: () => void;
+ cbToggleMeasurementMode: () => void;
+ cbGetMeasurementModeHandler: () => void;
+ cbDeleteVisibleShapeById: (id: number | string) => void;
+ cbUpdateAreaOfDrawingMeasurement: (area: string | null) => void;
+ cbSetCurrentDrawHandler: (handler: DrawHandler | null) => void;
+ cbSetMapStatus?: (status: string) => void;
+ visiblePolylines: (string | number)[];
+ localShapeStore: MeasurementShapeData[];
+ isDrawingEmpty: boolean;
+ nativeMove: boolean;
+ currenLine: DrawHandler | null;
+ polygonMode: boolean;
+ measurementMode: string | boolean;
+ startDrawing: boolean;
+ customTooltip: HTMLElement | null;
+ device: "desktop" | "mobile" | "tablet" | "Desktop" | null;
+ clickAfterShapeSelection: boolean;
+ snappingLatlng: LatLng | null;
+ snappingEnabled: boolean;
+ snappingQueryRadius?: number;
+ }
+ }
+
+ namespace control {
+ function measurePolygon(
+ options?: Partial
+ ): Control.MeasurePolygon;
+ }
+
+ interface Polyline {
+ customID?: number | string;
+ customShape?: string;
+ _path?: SVGPathElement;
+ _leaflet_id?: number;
+ _measurementLayer?: L.LayerGroup;
+ _measurementOptions?: any;
+ showMeasurements?: (options?: any) => this;
+ hideMeasurements?: () => this;
+ updateMeasurements?: () => void;
+ formatDistance?: (distance: number) => string;
+ formatArea?: (area: number) => string;
+ _getRotation?: (ll1: L.LatLng, ll2: L.LatLng) => number;
+ getCentroid?: (latlngs: L.LatLng[]) => L.LatLng;
+ enableEdit?: () => void;
+ disableEdit?: () => void;
+ }
+
+ interface Polygon {
+ customID?: number | string;
+ customShape?: string;
+ customHandle?: number;
+ _path?: SVGPathElement;
+ _measurementLayer?: L.LayerGroup;
+ _measurementOptions?: any;
+ showMeasurements?: (options?: any) => this;
+ hideMeasurements?: () => this;
+ updateMeasurements?: () => void;
+ formatDistance?: (distance: number) => string;
+ formatArea?: (area: number) => string;
+ _getRotation?: (ll1: L.LatLng, ll2: L.LatLng) => number;
+ getCentroid?: (latlngs: L.LatLng[]) => L.LatLng;
+ enableEdit?: () => void;
+ disableEdit?: () => void;
+ }
+
+ namespace Marker {
+ interface MeasurementOptions {
+ pane?: string;
+ showOnHover?: boolean;
+ minPixelDistance?: number;
+ showDistances?: boolean;
+ showArea?: boolean;
+ showTotalDistance?: boolean;
+ imperial?: boolean;
+ ha?: boolean;
+ formatDistance?: (distance: number) => string;
+ formatArea?: (area: number) => string;
+ lang?: {
+ totalLength?: string;
+ totalArea?: string;
+ segmentLength?: string;
+ };
+ }
+
+ class Measurement extends L.Layer {
+ options: MeasurementOptions;
+ _latlng: L.LatLng;
+ _measurement: string;
+ _title: string;
+ _rotation: number;
+ _map: L.Map;
+ _element: HTMLElement;
+
+ constructor(
+ latlng: L.LatLng,
+ measurement: string,
+ title: string,
+ rotation: number,
+ options?: MeasurementOptions
+ );
+
+ addTo(map: L.Map): this;
+ onAdd(map: L.Map): this;
+ onRemove(map: L.Map): this;
+ _setPosition(): void;
+ _animateZoom(opt: { zoom: number; center: L.LatLng }): void;
+ }
+ }
+
+ namespace marker {
+ function measurement(
+ latLng: L.LatLng,
+ measurement: string,
+ title: string,
+ rotation: number,
+ options?: Marker.MeasurementOptions
+ ): Marker.Measurement;
+ }
+
+ interface PolylineOptions {
+ showMeasurements?: boolean;
+ measurementOptions?: Marker.MeasurementOptions;
+ }
+
+ interface Marker {
+ customHandle?: number;
+ }
+
+ interface Layer {
+ customID?: number | string;
+ customHandle?: number;
+ _path?: SVGPathElement;
+ enableEdit?: () => void;
+ disableEdit?: () => void;
+ getLatLng?: () => LatLng;
+ _leaflet_id?: number;
+ }
+
+ interface Map {
+ _panes?: { popupPane: HTMLElement; [key: string]: HTMLElement };
+ }
+
+ namespace Draw {
+ interface DrawMap extends Map {
+ mergeOptions(options: any): void;
+ addInitHook(fn: () => void): void;
+ }
+ }
+
+ interface LeafletEvent {
+ layerType?: string;
+ layers?: LayerGroup;
+ }
}
diff --git a/playgrounds/geoportal-playground/src/app/components/map-measure/measure-path.js b/playgrounds/geoportal-playground/src/app/components/map-measure/measure-path.js
index ee4c7c5f64..2451026fab 100644
--- a/playgrounds/geoportal-playground/src/app/components/map-measure/measure-path.js
+++ b/playgrounds/geoportal-playground/src/app/components/map-measure/measure-path.js
@@ -8,7 +8,7 @@ L.Control.MeasurePolygon = L.Control.extend({
icon_polygonInactive: "https://img.icons8.com/?size=48&id=98463&format=png",
html_template: `Results
Area:
_p_area
-Perimeter :
_p_perimetro
`,
+Perimeter :
_p_perimeter
`,
height: 130,
width: 150,
mode_btn: "",
@@ -281,6 +281,10 @@ L.Control.MeasurePolygon = L.Control.extend({
this._measureLayers = L.layerGroup().addTo(map);
map.on("draw:created", (event) => {
+ console.log("[MEASURE] draw:created event fired", {
+ layer: event.layer,
+ shapeMode: this.options.shapeMode,
+ });
this.options.checkonedrawpoligon = false;
this.options.ifDrawing = false;
@@ -313,6 +317,7 @@ L.Control.MeasurePolygon = L.Control.extend({
});
map.on("draw:drawstart", (event) => {
+ console.log("[MEASURE] draw:drawstart event fired");
this.options.cbSaveLastActiveShapeIdBeforeDrawingHandler();
this.options.measurementOrder = this.options.measurementOrder + 1;
const shapesObj = {
@@ -326,6 +331,9 @@ L.Control.MeasurePolygon = L.Control.extend({
});
map.on("draw:drawvertex", (event) => {
+ console.log("[MEASURE] draw:drawvertex event fired", {
+ numLayers: Object.keys(event.layers._layers).length,
+ });
const layers = event.layers;
const latlngs = [];
let index = 0;
@@ -333,12 +341,32 @@ L.Control.MeasurePolygon = L.Control.extend({
layers.eachLayer((layer) => {
layer.customHandle = index++;
- layer.on("click", (e) => {
+
+ const clickHandler = (e) => {
+ console.log("[MEASURE] Vertex marker clicked:", {
+ customHandle: e.target.customHandle,
+ isFirstMarker: e.target.customHandle === 0,
+ shapeMode: this.options.shapeMode,
+ measureHandler: this._measureHandler,
+ latLng: e.latlng,
+ originalEvent: e.originalEvent,
+ });
if (e.target.customHandle === 0) {
+ console.log(
+ "[MEASURE] First marker clicked - attempting to close polygon"
+ );
+ console.log(
+ "[MEASURE] Before completeShape - markers:",
+ this._measureHandler._markers
+ ? this._measureHandler._markers.length
+ : "N/A"
+ );
this.options.shapeMode = "polygon";
this.options.currenLine.completeShape();
+ console.log("[MEASURE] After completeShape() called");
}
- });
+ };
+ layer.on("click", clickHandler);
layer.on("mouseover", (e) => {
const coordinates = this._measureHandler._poly._latlngs;
const latLngArray = coordinates.map((c) => [c.lat, c.lng]);
@@ -421,7 +449,7 @@ L.Control.MeasurePolygon = L.Control.extend({
return iconsWrapper;
},
- _UpdateAreaPerimetro: function (layer) {
+ _UpdateAreaperimeter: function (layer) {
const latlngs = layer.getLatLngs()[0];
const options = {
diff --git a/playgrounds/measurements-playground/index.html b/playgrounds/measurements-playground/index.html
deleted file mode 100644
index 0fb60ec454..0000000000
--- a/playgrounds/measurements-playground/index.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- Measurements playground
-
-
-
-
-
-
-
-
-
-
-
diff --git a/playgrounds/measurements-playground/postcss.config.cjs b/playgrounds/measurements-playground/postcss.config.cjs
deleted file mode 100644
index a5b8c605be..0000000000
--- a/playgrounds/measurements-playground/postcss.config.cjs
+++ /dev/null
@@ -1,13 +0,0 @@
-/* postcss.config.cjs */
-const path = require("path");
-
-module.exports = {
- plugins: {
- "postcss-import": {},
- "tailwindcss/nesting": {},
- tailwindcss: {
- config: path.join(__dirname, "tailwind.config.cjs"),
- },
- autoprefixer: {},
- },
-};
diff --git a/playgrounds/measurements-playground/project.json b/playgrounds/measurements-playground/project.json
deleted file mode 100644
index e75660e239..0000000000
--- a/playgrounds/measurements-playground/project.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
- "name": "measurements-playground",
- "$schema": "../../node_modules/nx/schemas/project-schema.json",
- "sourceRoot": "playgrounds/measurements-playground/src",
- "projectType": "application",
- "tags": [],
- "targets": {
- "build": {
- "executor": "@nx/vite:build",
- "outputs": ["{options.outputPath}"],
- "defaultConfiguration": "production",
- "options": {
- "outputPath": "dist/playgrounds/measurements-playground"
- },
- "configurations": {
- "development": {
- "mode": "development"
- },
- "production": {
- "mode": "production"
- }
- }
- },
- "serve": {
- "executor": "@nx/vite:dev-server",
- "defaultConfiguration": "development",
- "options": {
- "buildTarget": "measurements-playground:build"
- },
- "configurations": {
- "development": {
- "buildTarget": "measurements-playground:build:development",
- "hmr": true
- },
- "production": {
- "buildTarget": "measurements-playground:build:production",
- "hmr": false
- }
- }
- },
- "preview": {
- "executor": "@nx/vite:preview-server",
- "defaultConfiguration": "development",
- "options": {
- "buildTarget": "measurements-playground:build"
- },
- "configurations": {
- "development": {
- "buildTarget": "measurements-playground:build:development"
- },
- "production": {
- "buildTarget": "measurements-playground:build:production"
- }
- },
- "dependsOn": ["build"]
- },
- "test": {
- "executor": "@nx/vite:test",
- "outputs": ["{options.reportsDirectory}"],
- "options": {
- "reportsDirectory": "../../coverage/playgrounds/measurements-playground"
- }
- },
- "lint": {
- "executor": "@nx/eslint:lint"
- }
- }
-}
diff --git a/playgrounds/measurements-playground/public/favicon.ico b/playgrounds/measurements-playground/public/favicon.ico
deleted file mode 100644
index 317ebcb233..0000000000
Binary files a/playgrounds/measurements-playground/public/favicon.ico and /dev/null differ
diff --git a/playgrounds/measurements-playground/src/app/App.tsx b/playgrounds/measurements-playground/src/app/App.tsx
deleted file mode 100644
index bae018da7a..0000000000
--- a/playgrounds/measurements-playground/src/app/App.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import TopicMapComponent from "react-cismap/topicmaps/TopicMapComponent";
-import { suppressReactCismapErrors } from "@carma-commons/utils";
-import {
- MeasurementControl,
- Measurements,
- useMapMeasurementsContext,
- useMapLibreMap,
-} from "@carma-commons/measurements";
-import { ZoomControl } from "@carma-mapping/components";
-import { Control, ControlLayout } from "@carma-mapping/map-controls-layout";
-import { EmptySearchComponent } from "@carma-mapping/fuzzy-search";
-import { LibFuzzySearch } from "@carma-mapping/fuzzy-search";
-import { ResponsiveTopicMapContext } from "react-cismap/contexts/ResponsiveTopicMapContextProvider";
-import { useContext, useState } from "react";
-import {
- TopicMapSelectionContent,
- useSelectionTopicMap,
-} from "@carma-appframeworks/portals";
-import CismapLayer from "react-cismap/CismapLayer";
-import { getActionLinksForFeature } from "react-cismap/tools/uiHelper";
-import { TopicMapDispatchContext } from "react-cismap/contexts/TopicMapContextProvider";
-import { SnappingContext } from "../main";
-
-suppressReactCismapErrors(true);
-
-export function App({
- vectorStyles = [],
- onClearAllLayers,
-}: {
- vectorStyles?: any[];
- onClearAllLayers?: () => void;
-}) {
- const { responsiveState, gap, windowSize } = useContext(
- ResponsiveTopicMapContext
- ) as any;
- useSelectionTopicMap();
- const [selectedFeature, setSelectedFeature] = useState(undefined);
- const { maplibreMap, setMaplibreMap } = useMapLibreMap();
- const [maplibreMaps, setMaplibreMaps] = useState([]);
- const { mode: measurementMode, setMode: setMeasurementMode } =
- useMapMeasurementsContext();
- const { zoomToFeature } = useContext(TopicMapDispatchContext) as any;
- const { snappingEnabled, setSnappingEnabled } = useContext(SnappingContext);
-
- const pixelwidth =
- responsiveState === "normal" ? "300px" : (windowSize?.width || 300) - gap;
-
- let links: any[] = [];
- if (selectedFeature) {
- links = getActionLinksForFeature(selectedFeature, {
- displayZoomToFeature: true,
- zoomToFeature: () => {
- if (selectedFeature) {
- const f = JSON.stringify(selectedFeature, null, 2);
- const pf = JSON.parse(f);
- pf.crs = {
- type: "name",
- properties: {
- name: "urn:ogc:def:crs:EPSG::4326",
- },
- };
- console.log("xxx zoomToFeature", pf);
-
- zoomToFeature(pf);
- }
- },
- });
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* First row: Snapping toggle and Clear button */}
-
-
- setSnappingEnabled(e.target.checked)}
- style={{ cursor: "pointer" }}
- />
-
-
-
- {vectorStyles.length > 0 && (
- <>
-
-
- >
- )}
-
-
- {/* Second row: Vector layer links */}
-
-
-
-
-
-
-
-
- {vectorStyles.map((style, index) => {
- return (
- {
- setMaplibreMap(map);
- setMaplibreMaps((prev) => [...prev, map]);
- },
- }}
- />
- );
- })}
-
-
- );
-}
diff --git a/playgrounds/measurements-playground/src/app/components/ModeButtons.tsx b/playgrounds/measurements-playground/src/app/components/ModeButtons.tsx
deleted file mode 100644
index 8cd8d56e2a..0000000000
--- a/playgrounds/measurements-playground/src/app/components/ModeButtons.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * Mode selection buttons component
- */
-
-type Mode =
- | "features"
- | "coordinates"
- | "coordinatesUnderPointer"
- | "spider"
- | "spiderRocket"
- | "serious";
-
-interface ModeButtonsProps {
- mode: Mode | null;
- onModeChange: (mode: Mode) => void;
-}
-
-export function ModeButtons({ mode, onModeChange }: ModeButtonsProps) {
- const buttonStyle = (isActive: boolean) => ({
- padding: "8px 12px",
- border: "1px solid #ccc",
- borderRadius: "4px",
- background: isActive ? "#4CAF50" : "white",
- color: isActive ? "white" : "black",
- cursor: "pointer",
- fontSize: "14px",
- whiteSpace: "nowrap" as const,
- });
-
- return (
- <>
-
-
-
-
-
-
- >
- );
-}
diff --git a/playgrounds/measurements-playground/src/app/components/RadiusSliders.tsx b/playgrounds/measurements-playground/src/app/components/RadiusSliders.tsx
deleted file mode 100644
index 5f922ae7ed..0000000000
--- a/playgrounds/measurements-playground/src/app/components/RadiusSliders.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * Radius sliders component for query and tolerance radius
- */
-
-import { Slider } from "antd";
-
-type Mode =
- | "features"
- | "coordinates"
- | "coordinatesUnderPointer"
- | "spider"
- | "spiderRocket"
- | "serious";
-
-interface RadiusSlidersProps {
- queryRadius: number;
- toleranceRadius: number;
- mode: Mode | null;
- hasVectorLayer: boolean;
- onQueryRadiusChange: (value: number) => void;
- onToleranceRadiusChange: (value: number) => void;
-}
-
-export function RadiusSliders({
- queryRadius,
- toleranceRadius,
- mode,
- hasVectorLayer,
- onQueryRadiusChange,
- onToleranceRadiusChange,
-}: RadiusSlidersProps) {
- const showToleranceSlider =
- hasVectorLayer &&
- (mode === "spider" || mode === "spiderRocket" || mode === "serious");
-
- return (
- <>
- {/* Query Radius Slider */}
-
-
- onQueryRadiusChange(value)}
- />
-
-
- {/* Tolerance Radius Slider - only show for spider, spiderRocket, and serious modes */}
- {showToleranceSlider && (
-
-
- onToleranceRadiusChange(value)}
- />
-
- )}
- >
- );
-}
diff --git a/playgrounds/measurements-playground/src/app/components/VectorLayerButton.tsx b/playgrounds/measurements-playground/src/app/components/VectorLayerButton.tsx
deleted file mode 100644
index 86c3a707a8..0000000000
--- a/playgrounds/measurements-playground/src/app/components/VectorLayerButton.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Garbage can button to clear saved vector layer
- */
-
-interface VectorLayerButtonProps {
- hasSavedVectorStyle: boolean;
- onClear: () => void;
-}
-
-export function VectorLayerButton({
- hasSavedVectorStyle,
- onClear,
-}: VectorLayerButtonProps) {
- if (!hasSavedVectorStyle) {
- return null;
- }
-
- return (
-
- );
-}
diff --git a/playgrounds/measurements-playground/src/app/helper/constants.js b/playgrounds/measurements-playground/src/app/helper/constants.js
deleted file mode 100644
index 479fb27085..0000000000
--- a/playgrounds/measurements-playground/src/app/helper/constants.js
+++ /dev/null
@@ -1 +0,0 @@
-export const host = import.meta.env.VITE_WUPP_ASSET_BASEURL;
diff --git a/playgrounds/measurements-playground/src/app/store/index.ts b/playgrounds/measurements-playground/src/app/store/index.ts
deleted file mode 100644
index 5bfe910507..0000000000
--- a/playgrounds/measurements-playground/src/app/store/index.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { configureStore } from "@reduxjs/toolkit";
-
-import { createLogger } from "redux-logger";
-import { persistReducer } from "redux-persist";
-import localForage from "localforage";
-
-import mappingReducer from "./slices/mapping";
-import uiReducer from "./slices/ui";
-// import measurementsReducer from "./slices/measurements";
-
-console.info("store initializing ....");
-
-const devToolsEnabled =
- new URLSearchParams(window.location.search).get("devToolsEnabled") === "true";
-console.debug("devToolsEnabled:", devToolsEnabled);
-const stateLoggingEnabledFromSearch = new URLSearchParams(
- window.location.search
-).get("stateLoggingEnabled");
-
-const inProduction = process.env.NODE_ENV === "production";
-
-console.info("in Production Mode:", inProduction);
-const stateLoggingEnabled =
- (stateLoggingEnabledFromSearch !== null &&
- stateLoggingEnabledFromSearch !== "false") ||
- !inProduction;
-
-console.info(
- "stateLoggingEnabled:",
- stateLoggingEnabledFromSearch,
- "x",
- stateLoggingEnabled
-);
-const logger = createLogger({
- collapsed: true,
-});
-
-let middleware;
-if (stateLoggingEnabled === true) {
- middleware = (getDefaultMiddleware) =>
- getDefaultMiddleware({
- serializableCheck: false,
- }).concat(logger);
-} else {
- middleware = (getDefaultMiddleware) =>
- getDefaultMiddleware({
- serializableCheck: false,
- });
-}
-
-const uiConfig = {
- key: "@measurements-playground.app.config",
- storage: localForage,
- whitelist: [],
-};
-
-const mappingConfig = {
- key: "@measurements-playground.app.mapping",
- storage: localForage,
- whitelist: [],
-};
-
-// const measurementsConfig = {
-// key: "@measurements-playground.app.measurements",
-// storage: localForage,
-// whitelist: ["shapes"],
-// };
-
-const store = configureStore({
- reducer: {
- mapping: persistReducer(mappingConfig, mappingReducer),
- ui: persistReducer(uiConfig, uiReducer),
- // measurements: persistReducer(measurementsConfig, measurementsReducer),
- },
- devTools: devToolsEnabled === true && inProduction === false,
- middleware,
-});
-
-export type AppStore = typeof store;
-export type RootState = ReturnType;
-export type AppDispatch = AppStore["dispatch"];
-
-export default store;
diff --git a/playgrounds/measurements-playground/src/app/store/slices/mapping.ts b/playgrounds/measurements-playground/src/app/store/slices/mapping.ts
deleted file mode 100644
index 1a9e2ed735..0000000000
--- a/playgrounds/measurements-playground/src/app/store/slices/mapping.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { createSlice } from "@reduxjs/toolkit";
-import type { PayloadAction } from "@reduxjs/toolkit";
-
-import { RootState } from "..";
-
-const initialState = {
- startDrawing: false,
-};
-
-const slice = createSlice({
- name: "mapping",
- initialState,
- reducers: {
- setStartDrawing(state, action: PayloadAction) {
- state.startDrawing = action.payload;
- },
- },
-});
-
-export const { setStartDrawing } = slice.actions;
-export const getStartDrawing = (state: RootState) => state.mapping.startDrawing;
-
-export default slice.reducer;
diff --git a/playgrounds/measurements-playground/src/app/store/slices/ui.ts b/playgrounds/measurements-playground/src/app/store/slices/ui.ts
deleted file mode 100644
index 2e2d59c46f..0000000000
--- a/playgrounds/measurements-playground/src/app/store/slices/ui.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { createSlice } from "@reduxjs/toolkit";
-
-import type { PayloadAction } from "@reduxjs/toolkit";
-
-import { RootState } from "..";
-
-export enum UIMode {
- DEFAULT = "default",
- FEATURE_INFO = "featureInfo",
- MEASUREMENT = "measurement",
- PRINT = "print",
-}
-
-export interface UIState {
- mode: UIMode;
-}
-
-const initialState: UIState = {
- mode: UIMode.DEFAULT,
-};
-
-const slice = createSlice({
- name: "ui",
- initialState,
- reducers: {
- setUIMode(state, action) {
- state.mode = action.payload;
- },
- toggleUIMode(state, action: PayloadAction) {
- if (state.mode === action.payload) {
- state.mode = UIMode.DEFAULT;
- } else {
- state.mode = action.payload;
- }
- },
- },
-});
-
-export const { setUIMode, toggleUIMode } = slice.actions;
-
-export const getUIMode = (state: RootState) => state.ui.mode;
-
-export default slice.reducer;
diff --git a/playgrounds/measurements-playground/src/app/utils/coordinateExtraction.ts b/playgrounds/measurements-playground/src/app/utils/coordinateExtraction.ts
deleted file mode 100644
index 37d28b8d50..0000000000
--- a/playgrounds/measurements-playground/src/app/utils/coordinateExtraction.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * Utility functions for extracting and processing coordinates from GeoJSON features
- */
-
-/**
- * Extracts all coordinates from a collection of GeoJSON features as individual points
- */
-export function extractCoordinatesFromFeatures(
- features: any[],
- properties: Record = {}
-): any[] {
- const coordinatePoints: any[] = [];
-
- features.forEach((feature: any) => {
- const geometry = feature.geometry;
-
- // Extract coordinates based on geometry type
- if (geometry.type === "Point") {
- coordinatePoints.push({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: geometry.coordinates,
- },
- properties: { ...properties },
- });
- } else if (geometry.type === "LineString") {
- geometry.coordinates.forEach((coord: any) => {
- coordinatePoints.push({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: coord,
- },
- properties: { ...properties },
- });
- });
- } else if (geometry.type === "Polygon") {
- geometry.coordinates.forEach((ring: any) => {
- ring.forEach((coord: any) => {
- coordinatePoints.push({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: coord,
- },
- properties: { ...properties },
- });
- });
- });
- } else if (geometry.type === "MultiPoint") {
- geometry.coordinates.forEach((coord: any) => {
- coordinatePoints.push({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: coord,
- },
- properties: { ...properties },
- });
- });
- } else if (geometry.type === "MultiLineString") {
- geometry.coordinates.forEach((line: any) => {
- line.forEach((coord: any) => {
- coordinatePoints.push({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: coord,
- },
- properties: { ...properties },
- });
- });
- });
- } else if (geometry.type === "MultiPolygon") {
- geometry.coordinates.forEach((polygon: any) => {
- polygon.forEach((ring: any) => {
- ring.forEach((coord: any) => {
- coordinatePoints.push({
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: coord,
- },
- properties: { ...properties },
- });
- });
- });
- });
- }
- });
-
- return coordinatePoints;
-}
-
-/**
- * Filters points to only those within a specified pixel radius from a center point
- */
-export function filterPointsByRadius(
- points: any[],
- centerPoint: { x: number; y: number },
- radius: number,
- maplibreMap: any
-): any[] {
- return points.filter((pointFeature: any) => {
- const coord = pointFeature.geometry.coordinates;
- const projectedPoint = maplibreMap.project(coord);
-
- const dx = projectedPoint.x - centerPoint.x;
- const dy = projectedPoint.y - centerPoint.y;
- const distance = Math.sqrt(dx * dx + dy * dy);
-
- return distance <= radius;
- });
-}
-
-/**
- * Calculates distances for points and returns them with distance information
- */
-export function calculatePointDistances(
- points: any[],
- centerPoint: { x: number; y: number },
- maplibreMap: any
-): Array<{ pointFeature: any; distance: number }> {
- return points.map((pointFeature: any) => {
- const coord = pointFeature.geometry.coordinates;
- const projectedPoint = maplibreMap.project(coord);
-
- const dx = projectedPoint.x - centerPoint.x;
- const dy = projectedPoint.y - centerPoint.y;
- const distance = Math.sqrt(dx * dx + dy * dy);
-
- return { pointFeature, distance };
- });
-}
-
-/**
- * Finds the closest point from a list of points with distances
- */
-export function findClosestPoint(
- pointsWithDistance: Array<{ pointFeature: any; distance: number }>
-): { index: number; distance: number } | null {
- let shortestDistance = Infinity;
- let shortestIndex = -1;
-
- pointsWithDistance.forEach((item: any, index: number) => {
- if (item.distance < shortestDistance) {
- shortestDistance = item.distance;
- shortestIndex = index;
- }
- });
-
- if (shortestIndex === -1) {
- return null;
- }
-
- return { index: shortestIndex, distance: shortestDistance };
-}
-
-/**
- * Creates spider lines from a center point to multiple target points
- */
-export function createSpiderLines(
- centerCoords: [number, number],
- targetPoints: any[],
- properties: Record = {}
-): any[] {
- return targetPoints.map((pointFeature: any) => {
- return {
- type: "Feature",
- geometry: {
- type: "LineString",
- coordinates: [centerCoords, pointFeature.geometry.coordinates],
- },
- properties: { ...properties },
- };
- });
-}
diff --git a/playgrounds/measurements-playground/src/assets/.gitkeep b/playgrounds/measurements-playground/src/assets/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/playgrounds/measurements-playground/src/main.tsx b/playgrounds/measurements-playground/src/main.tsx
deleted file mode 100644
index 9e5033fa2d..0000000000
--- a/playgrounds/measurements-playground/src/main.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import { StrictMode, useEffect, useState, createContext } from "react";
-import * as ReactDOM from "react-dom/client";
-import { Provider, useSelector, useDispatch } from "react-redux";
-import { TopicMapContextProvider } from "react-cismap/contexts/TopicMapContextProvider";
-import store from "./app/store";
-import type { AppDispatch } from "./app/store";
-
-import { App } from "./app/App";
-
-import "./styles.css";
-import "bootstrap/dist/css/bootstrap.min.css";
-import "react-bootstrap-typeahead/css/Typeahead.css";
-import "leaflet/dist/leaflet.css";
-import { PersistGate } from "redux-persist/integration/react";
-import { persistStore } from "redux-persist";
-import {
- MapMeasurementsProvider,
- MEASUREMENT_MODE,
-} from "@carma-commons/measurements";
-import {
- GazDataProvider,
- SelectionProvider,
-} from "@carma-appframeworks/portals";
-import { getUIMode, setUIMode, UIMode } from "./app/store/slices/ui";
-
-// Context for snapping control
-export const SnappingContext = createContext<{
- snappingEnabled: boolean;
- setSnappingEnabled: (enabled: boolean) => void;
-}>({
- snappingEnabled: true,
- setSnappingEnabled: () => {},
-});
-
-// Wrapper component to connect Redux to MapMeasurementsProvider
-const MeasurementsProviderWrapper = ({
- children,
-}: {
- children: React.ReactNode;
-}) => {
- const uiMode = useSelector(getUIMode);
- const dispatch = useDispatch();
- const [snappingEnabled, setSnappingEnabled] = useState(true);
-
- const measurementsConfig = {
- // Only override what you want to change
- editableTitle: true,
- snappingEnabled: snappingEnabled,
- snappingOnUpdate: false,
- snappingRadiusVisible: true,
- debugOutputMapStatus: true,
- localStorageKey: "@MEASUREMENT_PLAYGROUNDY.app.measurements",
-
- // infoBoxHeaderColor: "#22c55e",
- };
-
- const mode =
- uiMode === UIMode.MEASUREMENT
- ? MEASUREMENT_MODE.MEASUREMENT
- : MEASUREMENT_MODE.DEFAULT;
- const handleSetMode = (newMode: MEASUREMENT_MODE) => {
- const newUIMode =
- newMode === MEASUREMENT_MODE.MEASUREMENT
- ? UIMode.MEASUREMENT
- : UIMode.DEFAULT;
- dispatch(setUIMode(newUIMode));
- };
-
- return (
-
-
- {children}
-
-
- );
-};
-
-// Root component with drag-and-drop functionality
-const RootComponent = () => {
- const [vectorStylesArray, setVectorStylesArray] = useState(() => {
- // Load from localStorage on mount
- const saved = localStorage.getItem("measurements-vector-style");
- if (saved) {
- try {
- const parsed = JSON.parse(saved);
- // Handle both old format (single object) and new format (array)
- return Array.isArray(parsed) ? parsed : [parsed];
- } catch (e) {
- console.error("Failed to parse saved vector style:", e);
- return [];
- }
- }
- return [];
- });
-
- useEffect(() => {
- const handleDrop = async (event: DragEvent) => {
- event.preventDefault();
-
- const url = event.dataTransfer?.getData("URL");
- console.log("handleDrop", url);
-
- if (url) {
- try {
- // Fetch the content of the URL
- const response = await fetch(url);
-
- if (response.ok) {
- const contentType = response.headers.get("Content-Type");
-
- if (contentType?.includes("application/json")) {
- const jsonData = await response.json();
- console.log("JSON content fetched:", jsonData);
-
- // Add to existing layers instead of replacing
- setVectorStylesArray((prev) => {
- const updatedArray = [...prev, jsonData];
- // Save updated array to localStorage
- localStorage.setItem(
- "measurements-vector-style",
- JSON.stringify(updatedArray)
- );
- return updatedArray;
- });
- } else {
- console.warn("The content is not JSON");
- }
- } else {
- console.error("Failed to fetch the URL:", response.statusText);
- }
- } catch (error) {
- console.error("Error fetching URL:", error);
- }
- } else if (
- event.dataTransfer?.files &&
- event.dataTransfer.files.length > 0
- ) {
- // Handle file drop
- const file = event.dataTransfer.files[0]; // Get the first dropped file
- console.log("File dropped:", file.name, file);
-
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- // Attempt to parse the file content as JSON
- const fileContent = e.target?.result;
- if (typeof fileContent === "string") {
- const processedContent = fileContent.replace(
- /__SERVER_URL__/g,
- "https://tiles.cismet.de"
- );
-
- const jsonData = JSON.parse(processedContent);
- console.log("Parsed JSON from file:", jsonData);
-
- // Add to existing layers instead of replacing
- setVectorStylesArray((prev) => {
- const updatedArray = [...prev, jsonData];
- // Save updated array to localStorage
- localStorage.setItem(
- "measurements-vector-style",
- JSON.stringify(updatedArray)
- );
- return updatedArray;
- });
- }
- } catch (error) {
- console.error("Failed to parse the file as JSON:", error);
- }
- };
-
- reader.readAsText(file); // Read the file as text
- }
- };
-
- const handleDragOver = (event: DragEvent) => {
- event.preventDefault();
- };
-
- window.addEventListener("drop", handleDrop);
- window.addEventListener("dragover", handleDragOver);
-
- return () => {
- window.removeEventListener("drop", handleDrop);
- window.removeEventListener("dragover", handleDragOver);
- };
- }, []);
-
- const clearAllVectorLayers = () => {
- setVectorStylesArray([]);
- localStorage.removeItem("measurements-vector-style");
- };
-
- return (
-
- );
-};
-
-const persistor = persistStore(store);
-
-const root = ReactDOM.createRoot(
- document.getElementById("root") as HTMLElement
-);
-root.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
diff --git a/playgrounds/measurements-playground/src/styles.css b/playgrounds/measurements-playground/src/styles.css
deleted file mode 100644
index db82083e9c..0000000000
--- a/playgrounds/measurements-playground/src/styles.css
+++ /dev/null
@@ -1,33 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-.leaflet-control * img {
- display: initial;
-}
-
-.ant-tooltip-arrow {
- display: none !important;
-}
-
-.modal-footer img[alt="Logo DigiTal Zwilling"] {
- max-height: 60px;
-}
-
-/* Make lists look good again */
-ul {
- list-style: disc !important;
- display: block;
- list-style-type: disc;
- margin-block-start: 1em;
- margin-block-end: 1em;
- margin-inline-start: 0px;
- margin-inline-end: 0px;
- padding-inline-start: 40px;
- unicode-bidi: isolate;
-}
-
-.modal-content svg,
-.modal-content img {
- display: inline;
-}
diff --git a/playgrounds/measurements-playground/tailwind.config.cjs b/playgrounds/measurements-playground/tailwind.config.cjs
deleted file mode 100644
index c70a51dd3e..0000000000
--- a/playgrounds/measurements-playground/tailwind.config.cjs
+++ /dev/null
@@ -1,15 +0,0 @@
-const { join } = require("path");
-const { workspaceRoot } = require('@nx/devkit');
-const { createGlobPatternsForDependencies } = require("@nx/react/tailwind");
-
-const preset = require(join(workspaceRoot, 'tailwind.preset.cjs'));
-
-const depsGlobs = createGlobPatternsForDependencies(__dirname);
-
-module.exports = {
- presets: [preset],
- content: [
- join(__dirname, "src/**/*!(*.stories|*.spec|*.test).{js,ts,jsx,tsx}"),
- ...depsGlobs,
- ],
-};
diff --git a/playgrounds/measurements-playground/tsconfig.json b/playgrounds/measurements-playground/tsconfig.json
deleted file mode 100644
index ca5385bfa4..0000000000
--- a/playgrounds/measurements-playground/tsconfig.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "extends": "../../tsconfig.legacy.base.json",
- "compilerOptions": {},
- "files": [],
- "references": []
-}
diff --git a/playgrounds/measurements-playground/tsconfig.spec.json b/playgrounds/measurements-playground/tsconfig.spec.json
deleted file mode 100644
index d6054e7f30..0000000000
--- a/playgrounds/measurements-playground/tsconfig.spec.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "outDir": "../../dist/out-tsc",
- "types": [
- "vitest/globals",
- "vitest/importMeta",
- "vite/client",
- "node",
- "vitest",
- "@nx/react/typings/cssmodule.d.ts",
- "@nx/react/typings/image.d.ts"
- ]
- },
- "include": [
- "vite.config.ts",
- "vitest.config.ts",
- "src/**/*.test.ts",
- "src/**/*.spec.ts",
- "src/**/*.test.tsx",
- "src/**/*.spec.tsx",
- "src/**/*.test.js",
- "src/**/*.spec.js",
- "src/**/*.test.jsx",
- "src/**/*.spec.jsx",
- "src/**/*.d.ts"
- ]
-}
diff --git a/playgrounds/measurements-playground/vite.config.ts b/playgrounds/measurements-playground/vite.config.ts
deleted file mode 100644
index ee050b1a48..0000000000
--- a/playgrounds/measurements-playground/vite.config.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-///
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
-
-// Use an environment variable to set the base URL
-const base = process.env.BASE_URL || "/";
-
-export default defineConfig({
- root: __dirname,
- cacheDir: "../../node_modules/.vite/playgrounds/fuzzy-search-playground",
-
- server: {
- port: 4200,
- host: "localhost",
- fs: {
- allow: ["../../"],
- },
- },
-
- preview: {
- port: 4300,
- host: "localhost",
- },
-
- plugins: [react(), nxViteTsPaths()],
- base: base,
- // Uncomment this if you are using workers.
- // worker: {
- // plugins: [ nxViteTsPaths() ],
- // },
-
- build: {
- outDir: "../../dist/playgrounds/fuzzy-search-playground",
- reportCompressedSize: true,
- commonjsOptions: {
- transformMixedEsModules: true,
- },
- },
-
- test: {
- globals: true,
- cache: {
- dir: "../../node_modules/.vitest",
- },
- environment: "jsdom",
- include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
-
- reporters: ["default"],
- coverage: {
- reportsDirectory: "../../coverage/playgrounds/stadtplan-playground",
- provider: "v8",
- },
- },
-});