@m4l/gclick 0.3.0 → 0.3.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/components/Device/slots/DeviceSlots.d.ts +2 -2
  2. package/components/DeviceLabel/slots/DeviceLabelSlots.d.ts +1 -1
  3. package/components/VideoTimeLineSelector/slots/VideoTimeLineSelectorSlots.d.ts +2 -2
  4. package/components/indicators/IndicatorBattery/slots/IndicatorBaterySlots.d.ts +1 -1
  5. package/components/indicators/IndicatorCSQ/slots/IndicatorCSQSlots.d.ts +1 -1
  6. package/components/indicators/IndicatorValueAndMagnitude/slots/styled.d.ts +2 -2
  7. package/components/indicators/IndicatorValueStatus/slots/IndicatorValueStatusSlots.d.ts +1 -1
  8. package/components/maps/components/GpsMap/contexts/MapContext/index.d.ts +1 -1
  9. package/components/maps/components/GpsMap/contexts/MapContext/store.js +13 -1
  10. package/components/maps/components/GpsMap/contexts/MapContext/types.d.ts +54 -1
  11. package/components/maps/components/GpsMap/hooks/index.d.ts +4 -0
  12. package/components/maps/components/GpsMap/hooks/useAutoFocus/index.js +3 -2
  13. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/GridDebugVisualization.d.ts +9 -0
  14. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/constants.d.ts +5 -0
  15. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/constants.js +12 -0
  16. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/helpers/decimateByConcentrationGrid.d.ts +13 -0
  17. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/helpers/decimateByConcentrationGrid.js +57 -0
  18. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/helpers/decimateByPixelDistanceAlongPath.d.ts +14 -0
  19. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/helpers/decimateByPixelDistanceAlongPath.js +24 -0
  20. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/helpers/index.d.ts +2 -0
  21. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/helpers/index.js +1 -0
  22. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/index.d.ts +64 -0
  23. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/index.js +117 -0
  24. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/createPixelPathDecimationFilter/types.d.ts +41 -0
  25. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/kdbush.d.ts +15 -0
  26. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/rbush.d.ts +19 -0
  27. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/types.d.ts +83 -0
  28. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/useFilterDecimation.d.ts +11 -0
  29. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/useFilterDecimation.js +112 -0
  30. package/components/maps/components/GpsMap/hooks/useZoomBasedMarkerFilter/useGridDebug.d.ts +5 -0
  31. package/components/maps/components/GpsMap/popups/MapPopupDevice/slots/MapPopupDeviceSlots.d.ts +3 -3
  32. package/components/maps/components/GpsMap/slots/styled.d.ts +1 -1
  33. package/components/maps/components/GpsMap/subcomponents/Controls/subcomponents/ButtonsToolsList/subcomponents/MapSourcesTool/slots/styled.d.ts +1 -1
  34. package/components/maps/components/GpsMap/subcomponents/Controls/subcomponents/ButtonsToolsList/subcomponents/MeasureTool/slots/styled.d.ts +2 -2
  35. package/components/maps/components/GpsMap/subcomponents/Layers/subcomponents/Layer/index.js +25 -3
  36. package/formatters/CourseFormatter/slots/CourseFormatterSlots.d.ts +1 -1
  37. package/index.js +20 -16
  38. package/not_recognized/index.js +241 -0
  39. package/package.json +18 -2
@@ -17,7 +17,7 @@ function useAutoFocus() {
17
17
  return false;
18
18
  }).map((layerId) => ({
19
19
  layerId,
20
- features: (function() {
20
+ features: function() {
21
21
  if (state.hashLayers[layerId].geoJsonObject?.object.type === "FeatureCollection") {
22
22
  const layer = state.hashLayers[layerId].geoJsonObject?.object;
23
23
  return layer.features.filter((feature) => feature.layerData !== void 0).map((feature) => ({ featureId: feature.id, layerData: feature.layerData }));
@@ -28,10 +28,11 @@ function useAutoFocus() {
28
28
  }
29
29
  }
30
30
  return [];
31
- })()
31
+ }()
32
32
  }));
33
33
  return result;
34
34
  }, deepShallow);
35
+ console.log("layerWithAutoFocus", layerWithAutoFocus);
35
36
  useEffect(() => {
36
37
  const proccesLayerWithAutoFocus = (layersFoucus) => {
37
38
  const layersToFitBounds = [];
@@ -0,0 +1,9 @@
1
+ import { GridCell } from './types';
2
+ interface GridDebugVisualizationProps {
3
+ cells: GridCell[];
4
+ }
5
+ /**
6
+ * Componente temporal para visualizar la grilla de muestreo (solo para debug)
7
+ */
8
+ export declare function GridDebugVisualization({ cells }: GridDebugVisualizationProps): null;
9
+ export {};
@@ -0,0 +1,5 @@
1
+ import { ZoomBasedFilterConfig } from './types';
2
+ import { PixelPathFilterConfig } from './createPixelPathDecimationFilter/types';
3
+ export declare const DEFAULT_CONFIG: Required<ZoomBasedFilterConfig>;
4
+ export declare const PIXEL_PATH_DEFAULT_CONFIG: Required<PixelPathFilterConfig>;
5
+ export declare const USE_FILTER_DECIMATION_DEFAULT_DEBOUNCE_MS = 150;
@@ -0,0 +1,12 @@
1
+ const PIXEL_PATH_DEFAULT_CONFIG = {
2
+ enabled: true,
3
+ viewportOffsetFactor: 1.2,
4
+ minPixelDistance: 40,
5
+ alwaysShowFirstAndLast: true,
6
+ maxMarkers: 500
7
+ };
8
+ const USE_FILTER_DECIMATION_DEFAULT_DEBOUNCE_MS = 150;
9
+ export {
10
+ PIXEL_PATH_DEFAULT_CONFIG as P,
11
+ USE_FILTER_DECIMATION_DEFAULT_DEBOUNCE_MS as U
12
+ };
@@ -0,0 +1,13 @@
1
+ import { FeatureWithCoords } from '../types';
2
+ /**
3
+ * Decima un conjunto de IDs cuando superan `maxMarkers`, usando una grilla espacial sobre
4
+ * el bbox (minLng, minLat, maxLng, maxLat).
5
+ *
6
+ * Divide el área en una grilla de aproximadamente sqrt(maxMarkers) × sqrt(maxMarkers) celdas
7
+ * y mantiene como máximo una marca por celda. Preserva el orden original y, si se pasa
8
+ * `firstAndLast`, prioriza siempre el primer y último punto del path.
9
+ */
10
+ export declare function decimateByConcentrationGrid(orderedIds: (string | number)[], idToPoint: Map<string | number, FeatureWithCoords>, minLng: number, minLat: number, maxLng: number, maxLat: number, maxMarkers: number, firstAndLast?: {
11
+ firstId: string | number;
12
+ lastId: string | number;
13
+ }): (string | number)[];
@@ -0,0 +1,57 @@
1
+ function decimateByConcentrationGrid(orderedIds, idToPoint, minLng, minLat, maxLng, maxLat, maxMarkers, firstAndLast) {
2
+ if (orderedIds.length <= maxMarkers) {
3
+ return orderedIds;
4
+ }
5
+ const spanLng = maxLng - minLng;
6
+ const spanLat = maxLat - minLat;
7
+ if (spanLng <= 0 || spanLat <= 0) {
8
+ return orderedIds.slice(0, maxMarkers);
9
+ }
10
+ const g = Math.max(1, Math.floor(Math.sqrt(maxMarkers)));
11
+ const filledCells = /* @__PURE__ */ new Set();
12
+ const added = /* @__PURE__ */ new Set();
13
+ const result = [];
14
+ function cellKey(lat, lng) {
15
+ const col = Math.min(g - 1, Math.max(0, Math.floor((lng - minLng) / spanLng * g)));
16
+ const row = Math.min(g - 1, Math.max(0, Math.floor((lat - minLat) / spanLat * g)));
17
+ return row * g + col;
18
+ }
19
+ if (firstAndLast) {
20
+ const { firstId, lastId } = firstAndLast;
21
+ for (const id of [firstId, lastId]) {
22
+ if (id === void 0 || added.has(id)) {
23
+ continue;
24
+ }
25
+ const p = idToPoint.get(id);
26
+ if (p === void 0) {
27
+ continue;
28
+ }
29
+ added.add(id);
30
+ result.push(id);
31
+ filledCells.add(cellKey(p.lat, p.lng));
32
+ }
33
+ }
34
+ for (const id of orderedIds) {
35
+ if (added.has(id)) {
36
+ continue;
37
+ }
38
+ if (result.length >= maxMarkers) {
39
+ break;
40
+ }
41
+ const p = idToPoint.get(id);
42
+ if (p === void 0) {
43
+ continue;
44
+ }
45
+ const key = cellKey(p.lat, p.lng);
46
+ if (filledCells.has(key)) {
47
+ continue;
48
+ }
49
+ filledCells.add(key);
50
+ added.add(id);
51
+ result.push(id);
52
+ }
53
+ return result.sort((a, b) => orderedIds.indexOf(a) - orderedIds.indexOf(b));
54
+ }
55
+ export {
56
+ decimateByConcentrationGrid as d
57
+ };
@@ -0,0 +1,14 @@
1
+ import { Map as LeafletMap } from 'leaflet';
2
+ import { FeatureWithCoords } from '../types';
3
+ /**
4
+ * Decima una secuencia ordenada de puntos manteniendo solo aquellos que distan al menos
5
+ * `minPixelDistance` píxeles del anterior en el path (en coordenadas de pantalla).
6
+ *
7
+ * Recorre `indicesInRouteOrder` (orden del recorrido), proyecta cada punto a píxeles con
8
+ * map.latLngToLayerPoint, y añade al resultado solo si la distancia euclídea al último
9
+ * punto seleccionado supera el umbral. Usa distancia al cuadrado para evitar Math.sqrt.
10
+ *
11
+ * Si `alwaysShowFirstAndLast` es true, garantiza que el primer y último punto del path
12
+ * estén en el resultado aunque no cumplan la distancia mínima.
13
+ */
14
+ export declare function decimateByPixelDistanceAlongPath(map: LeafletMap, indicesInRouteOrder: number[], points: FeatureWithCoords[], minPixelDistance: number, alwaysShowFirstAndLast: boolean): (string | number)[];
@@ -0,0 +1,24 @@
1
+ function decimateByPixelDistanceAlongPath(map, indicesInRouteOrder, points, minPixelDistance, alwaysShowFirstAndLast) {
2
+ const selected = [];
3
+ let lastPx = null;
4
+ const minPixelDistanceSq = minPixelDistance * minPixelDistance;
5
+ for (const idx of indicesInRouteOrder) {
6
+ const p = points[idx];
7
+ const px = map.latLngToLayerPoint([p.lat, p.lng]);
8
+ if (lastPx === null) {
9
+ selected.push(p.id);
10
+ lastPx = { x: px.x, y: px.y };
11
+ continue;
12
+ }
13
+ const dx = px.x - lastPx.x;
14
+ const dy = px.y - lastPx.y;
15
+ if (dx * dx + dy * dy >= minPixelDistanceSq) {
16
+ selected.push(p.id);
17
+ lastPx = { x: px.x, y: px.y };
18
+ }
19
+ }
20
+ return selected;
21
+ }
22
+ export {
23
+ decimateByPixelDistanceAlongPath as d
24
+ };
@@ -0,0 +1,2 @@
1
+ export { decimateByConcentrationGrid } from './decimateByConcentrationGrid';
2
+ export { decimateByPixelDistanceAlongPath } from './decimateByPixelDistanceAlongPath';
@@ -0,0 +1,64 @@
1
+ import { DecimationFilter, PixelPathFilterConfig } from './types';
2
+ /**
3
+ * Factory que crea una función de decimación para reducir marcadores en capas de mapa
4
+ * con secuencias ordenadas de puntos (p. ej. historial GPS, rutas, waypoints).
5
+ *
6
+ * ## Propósito
7
+ *
8
+ * Cuando una capa tiene miles de puntos (p. ej. un recorrido GPS de muchas horas),
9
+ * renderizar todos degrada el rendimiento. Esta función retorna un filtro que reduce
10
+ * los puntos visibles manteniendo la forma visual del path y priorizando los que
11
+ * están en el viewport actual.
12
+ *
13
+ * ## Algoritmo (en orden)
14
+ *
15
+ * 1. **Índice espacial (KDBush)**: Construye un índice para consultas de rango O(log n).
16
+ * Se cachea mientras featureIds/featuresWithCoords no cambien.
17
+ *
18
+ * 2. **Orden del path**: Ordena los puntos según el orden de featureIds (orden temporal
19
+ * del recorrido, no espacial).
20
+ *
21
+ * 3. **Decimación por distancia en píxeles**: Recorre el path en orden y mantiene solo
22
+ * puntos que distan al menos `minPixelDistance` píxeles del anterior. Evita
23
+ * saturar zonas con muchos puntos cercanos.
24
+ *
25
+ * 4. **Filtro por viewport**: Usa KDBush.range() para quedarse solo con puntos dentro
26
+ * del bounds visible (ampliado por viewportOffsetFactor). Los que están fuera no
27
+ * se renderizan.
28
+ *
29
+ * 5. **Primero y último del path**: Si `alwaysShowFirstAndLast`, añade el primer y
30
+ * último punto del recorrido solo si están en viewport, aunque no cumplan la distancia
31
+ * mínima, para no cortar visualmente el inicio/fin.
32
+ *
33
+ * 6. **Límite por concentración (opcional)**: Si tras lo anterior hay más de `maxMarkers`,
34
+ * aplica una grilla espacial para distribuir las marcas y no saturar zonas densas.
35
+ *
36
+ * ## Uso típico
37
+ *
38
+ * ```ts
39
+ * addLayer({
40
+ * layerId: 'points',
41
+ * renderFeature: HistoryMarkerRender,
42
+ * decimationConfig: {
43
+ * decimationFilter: createPixelPathDecimationFilter({
44
+ * minPixelDistance: 80,
45
+ * maxMarkers: 500,
46
+ * }),
47
+ * },
48
+ * });
49
+ * ```
50
+ *
51
+ * ## Cache
52
+ *
53
+ * Si el hook pasa `params.cache`, se reutilizan:
54
+ * - El índice KDBush y los mapas auxiliares entre llamadas (zoom/pan).
55
+ * - El resultado de decimateByPixelDistanceAlongPath **por nivel de zoom**: al volver
56
+ * a un zoom ya visitado, se devuelve el resultado cacheado sin recalcular.
57
+ *
58
+ * La cache se invalida cuando cambian featureIds o featuresWithCoords.
59
+ * @param config - Opciones de decimación (minPixelDistance, viewportOffsetFactor, etc.).
60
+ * Si no se pasa, se usan los valores por defecto.
61
+ * @returns Una DecimationFilter que recibe viewport, zoom, featureIds, etc. y devuelve
62
+ * los featureIds que deben renderizarse.
63
+ */
64
+ export declare function createPixelPathDecimationFilter(config?: PixelPathFilterConfig): DecimationFilter;
@@ -0,0 +1,117 @@
1
+ import { K as KDBush } from "../../../../../../../not_recognized/index.js";
2
+ import { P as PIXEL_PATH_DEFAULT_CONFIG } from "../constants.js";
3
+ import { d as decimateByPixelDistanceAlongPath } from "./helpers/decimateByPixelDistanceAlongPath.js";
4
+ import { d as decimateByConcentrationGrid } from "./helpers/decimateByConcentrationGrid.js";
5
+ function createPixelPathDecimationFilter(config) {
6
+ const mergedConfig = { ...PIXEL_PATH_DEFAULT_CONFIG, ...config };
7
+ return (params) => {
8
+ const { featureIds, bounds, map, featuresWithCoords, cache, zoom } = params;
9
+ if (featureIds.length === 0) {
10
+ return [];
11
+ }
12
+ if (featuresWithCoords.length === 0) {
13
+ return [...featureIds];
14
+ }
15
+ const points = featuresWithCoords;
16
+ const factor = mergedConfig.viewportOffsetFactor ?? 1;
17
+ const latHalf = (bounds.getNorth() - bounds.getSouth()) / 2;
18
+ const lngHalf = (bounds.getEast() - bounds.getWest()) / 2;
19
+ const minLng = (bounds.getWest() + bounds.getEast()) / 2 - lngHalf * factor;
20
+ const minLat = (bounds.getSouth() + bounds.getNorth()) / 2 - latHalf * factor;
21
+ const maxLng = (bounds.getWest() + bounds.getEast()) / 2 + lngHalf * factor;
22
+ const maxLat = (bounds.getSouth() + bounds.getNorth()) / 2 + latHalf * factor;
23
+ const inBounds = (lat, lng) => lat >= minLat && lat <= maxLat && lng >= minLng && lng <= maxLng;
24
+ const computePrecomputed = () => {
25
+ const idToRouteIndexMap = /* @__PURE__ */ new Map();
26
+ for (let i = 0; i < featureIds.length; i++) {
27
+ idToRouteIndexMap.set(featureIds[i], i);
28
+ }
29
+ const allIndices = Array.from({ length: points.length }, (_, i) => i).sort(
30
+ (a, b) => (idToRouteIndexMap.get(points[a].id) ?? -1) - (idToRouteIndexMap.get(points[b].id) ?? -1)
31
+ );
32
+ const idToPointMap = new Map(
33
+ points.map((p) => [p.id, p])
34
+ );
35
+ const sortedPoints = [...points].sort((a, b) => {
36
+ if (a.lat !== b.lat) {
37
+ return a.lat - b.lat;
38
+ }
39
+ if (a.lng !== b.lng) {
40
+ return a.lng - b.lng;
41
+ }
42
+ return String(a.id).localeCompare(String(b.id));
43
+ });
44
+ const kdbushIndex = new KDBush(sortedPoints.length, 9);
45
+ sortedPoints.forEach((p) => kdbushIndex.add(p.lng, p.lat));
46
+ kdbushIndex.finish();
47
+ return {
48
+ idToRouteIndex: idToRouteIndexMap,
49
+ allIndicesInRouteOrder: allIndices,
50
+ idToPoint: idToPointMap,
51
+ spatialIndex: { index: kdbushIndex, points: sortedPoints }
52
+ };
53
+ };
54
+ const {
55
+ idToRouteIndex,
56
+ allIndicesInRouteOrder,
57
+ idToPoint,
58
+ spatialIndex
59
+ } = cache ? cache.get("pixelPathDecimation", computePrecomputed) : computePrecomputed();
60
+ const minPixelDistance = mergedConfig.minPixelDistance ?? 40;
61
+ const alwaysShowFirstAndLast = mergedConfig.alwaysShowFirstAndLast ?? true;
62
+ const computeDecimatedByZoom = () => decimateByPixelDistanceAlongPath(
63
+ map,
64
+ allIndicesInRouteOrder,
65
+ points,
66
+ minPixelDistance
67
+ );
68
+ const decimatedIds = cache !== void 0 && cache !== null ? cache.get(`pixelPathDecimation_decimated_zoom_${zoom}`, computeDecimatedByZoom) : computeDecimatedByZoom();
69
+ const indicesInViewport = spatialIndex.index.range(
70
+ minLng,
71
+ minLat,
72
+ maxLng,
73
+ maxLat
74
+ );
75
+ const idsInViewport = new Set(
76
+ indicesInViewport.map((i) => spatialIndex.points[i].id)
77
+ );
78
+ const inViewDecimated = decimatedIds.filter((id) => idsInViewport.has(id));
79
+ let result = inViewDecimated;
80
+ if (alwaysShowFirstAndLast && allIndicesInRouteOrder.length > 0) {
81
+ const firstPoint = points[allIndicesInRouteOrder[0]];
82
+ const lastPoint = points[allIndicesInRouteOrder[allIndicesInRouteOrder.length - 1]];
83
+ const firstId = firstPoint.id;
84
+ const lastId = lastPoint.id;
85
+ const set = new Set(result);
86
+ const toPrepend = [];
87
+ const toAppend = [];
88
+ if (inBounds(firstPoint.lat, firstPoint.lng) && !set.has(firstId)) {
89
+ toPrepend.push(firstId);
90
+ }
91
+ if (lastId !== firstId && inBounds(lastPoint.lat, lastPoint.lng) && !set.has(lastId)) {
92
+ toAppend.push(lastId);
93
+ }
94
+ result = [...toPrepend, ...result, ...toAppend];
95
+ }
96
+ let ordered = [...result].sort(
97
+ (a, b) => (idToRouteIndex.get(a) ?? -1) - (idToRouteIndex.get(b) ?? -1)
98
+ );
99
+ const maxMarkers = mergedConfig.maxMarkers;
100
+ if (maxMarkers !== void 0 && maxMarkers !== null && maxMarkers > 0 && ordered.length > maxMarkers) {
101
+ ordered = decimateByConcentrationGrid(
102
+ ordered,
103
+ idToPoint,
104
+ minLng,
105
+ minLat,
106
+ maxLng,
107
+ maxLat,
108
+ maxMarkers,
109
+ alwaysShowFirstAndLast && ordered.length > 0 ? { firstId: ordered[0], lastId: ordered[ordered.length - 1] } : void 0
110
+ );
111
+ }
112
+ return ordered;
113
+ };
114
+ }
115
+ export {
116
+ createPixelPathDecimationFilter as c
117
+ };
@@ -0,0 +1,41 @@
1
+ export type { DecimationFilter, DecimationFilterParams, FeatureWithCoords, } from '../../../contexts/MapContext/types';
2
+ /**
3
+ * Configuración para el filtro de decimación basado en distancia en píxeles a lo largo del path.
4
+ *
5
+ * Usado por `createPixelPathDecimationFilter` para reducir marcadores en capas con secuencias
6
+ * ordenadas de puntos (recorridos GPS, rutas). El filtro se recalcula en zoom/pan (con debounce);
7
+ * respeta la distancia mínima entre marcas consecutivas y opcionalmente aplica un tope máximo
8
+ * mediante grilla espacial.
9
+ */
10
+ export interface PixelPathFilterConfig {
11
+ /**
12
+ * Si es false, el filtro no se aplica y se devuelven todos los featureIds sin filtrar.
13
+ * Por defecto true (filtrado activo).
14
+ */
15
+ enabled?: boolean;
16
+ /**
17
+ * Factor de expansión del viewport para el bbox de consulta espacial.
18
+ * 1 = solo área visible; 1.2 = 20% más en cada dirección (evita marcas que aparecen/desaparecen
19
+ * en el borde al hacer pan).
20
+ */
21
+ viewportOffsetFactor?: number;
22
+ /**
23
+ * Distancia mínima en píxeles entre marcas consecutivas a lo largo del path (orden featureIds).
24
+ * Puntos más cercanos se descartan para evitar saturación visual. Valores típicos: 40–100.
25
+ */
26
+ minPixelDistance?: number;
27
+ /**
28
+ * Si true, siempre incluye el primer y último punto del path aunque no cumplan minPixelDistance,
29
+ * para no cortar visualmente el inicio/fin del recorrido.
30
+ *
31
+ * Solo se añaden cuando están dentro del viewport visible (ampliado por viewportOffsetFactor).
32
+ * Si están fuera del área visible, no se renderizan.
33
+ */
34
+ alwaysShowFirstAndLast?: boolean;
35
+ /**
36
+ * Límite máximo de marcas a mostrar. Si tras la decimación por path y viewport hay más, se aplica
37
+ * decimación por concentración (grilla espacial: como máximo una marca por celda).
38
+ * Opcional; si no se define, no se aplica tope.
39
+ */
40
+ maxMarkers?: number;
41
+ }
@@ -0,0 +1,15 @@
1
+ declare module 'kdbush' {
2
+ /**
3
+ *
4
+ */
5
+ export default class KDBush {
6
+ constructor(numItems: number, nodeSize?: number, ArrayType?: typeof Float64Array);
7
+ add(x: number, y: number): number;
8
+ finish(): this;
9
+ range(minX: number, minY: number, maxX: number, maxY: number): number[];
10
+ within(x: number, y: number, radius: number): number[];
11
+ readonly numItems: number;
12
+ readonly nodeSize: number;
13
+ static from(data: ArrayBuffer): KDBush;
14
+ }
15
+ }
@@ -0,0 +1,19 @@
1
+ declare module 'rbush' {
2
+ interface BBox {
3
+ minX: number;
4
+ minY: number;
5
+ maxX: number;
6
+ maxY: number;
7
+ }
8
+
9
+ /**
10
+ *
11
+ */
12
+ class RBush<T extends BBox = BBox> {
13
+ load(items: T[]): this;
14
+ search(bbox: BBox): T[];
15
+ clear(): this;
16
+ }
17
+
18
+ export default RBush;
19
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Configuración para el filtrado de marcas basado en zoom y viewport (RBush)
3
+ */
4
+ export interface ZoomBasedFilterConfig {
5
+ /**
6
+ * Nivel de zoom mínimo donde se muestran todas las marcas en viewport
7
+ * Por debajo de este zoom, se aplica el filtrado
8
+ */
9
+ minZoomForAllMarkers?: number;
10
+ /**
11
+ * Límite máximo de marcas a mostrar cuando el zoom está alejado
12
+ */
13
+ maxMarkersAtMinZoom?: number;
14
+ /**
15
+ * Si es true, usa muestreo espacial (grid) cuando hay más marcas que el límite
16
+ */
17
+ useSpatialSampling?: boolean;
18
+ /**
19
+ * Si es true, retorna información de la grilla para visualización (solo para debug)
20
+ */
21
+ showGridDebug?: boolean;
22
+ /**
23
+ * Factor de expansión del viewport para el bounding box (1 = solo visible, 1.2 = 20% más en cada lado)
24
+ */
25
+ viewportOffsetFactor?: number;
26
+ /**
27
+ * Milisegundos de debounce para moveend/zoomend (evita saturar el pintado en movimientos intensos)
28
+ */
29
+ debounceMs?: number;
30
+ /**
31
+ * Si es true, usa decimación por distancia (geokdbush): mínima distancia entre marcas según zoom.
32
+ * Más fluido que grilla: a mayor zoom menos distancia mínima = más marcas.
33
+ */
34
+ useDistanceDecimation?: boolean;
35
+ /**
36
+ * Distancia mínima entre marcas (m) cuando el zoom está bajo. A mayor zoom la distancia se reduce.
37
+ * Solo aplica si useDistanceDecimation es true.
38
+ */
39
+ minDistanceMetersAtLowZoom?: number;
40
+ /**
41
+ * Distancia mínima entre marcas (m) cuando el zoom está alto.
42
+ * Solo aplica si useDistanceDecimation es true.
43
+ */
44
+ minDistanceMetersAtHighZoom?: number;
45
+ /**
46
+ * Nivel de zoom considerado "bajo" para minDistanceMetersAtLowZoom.
47
+ */
48
+ lowZoomLevel?: number;
49
+ /**
50
+ * Nivel de zoom considerado "alto" para minDistanceMetersAtHighZoom.
51
+ */
52
+ highZoomLevel?: number;
53
+ /**
54
+ * Si es true, al decimar se distribuyen los puntos a lo largo de la distancia total del recorrido
55
+ * (orden de featureIds), no se toman solo los primeros. Evita que queden "pegados" al inicio.
56
+ */
57
+ distributeByPathDistance?: boolean;
58
+ }
59
+ /**
60
+ * Configuración transversal del hook useFilterDecimation.
61
+ * Afecta cómo se programa la recomputación ante zoom/pan, no a la lógica de decimación.
62
+ */
63
+ export interface UseFilterDecimationConfig {
64
+ /**
65
+ * Debounce en ms antes de recalcular tras zoomend/moveend.
66
+ * Si hay zoom o movimiento continuo, se cancela el cálculo pendiente y se programa uno nuevo.
67
+ * 0 = sin debounce (ejecución inmediata). Por defecto 150.
68
+ */
69
+ debounceMs?: number;
70
+ }
71
+ /**
72
+ * Información de una celda de la grilla para visualización
73
+ */
74
+ export interface GridCell {
75
+ bounds: {
76
+ north: number;
77
+ south: number;
78
+ east: number;
79
+ west: number;
80
+ };
81
+ featureCount: number;
82
+ selected: boolean;
83
+ }
@@ -0,0 +1,11 @@
1
+ import { DecimationFilter } from '../../contexts/MapContext/types';
2
+ import { UseFilterDecimationConfig } from './types';
3
+ /**
4
+ * Hook que filtra featureIds según la función decimationFilter de la capa.
5
+ * Si decimationFilter está definida: la invoca con viewport, zoom, featureIds, etc.
6
+ * en cada zoom/pan y retorna los ids filtrados.
7
+ * Si no está definida: retorna todos los featureIds sin filtrar.
8
+ *
9
+ * config: configuración transversal (debounce en zoom/pan), no afecta la lógica de decimación.
10
+ */
11
+ export declare function useFilterDecimation(featureIds: (string | number)[], layerId: string, decimationFilter?: DecimationFilter, config?: UseFilterDecimationConfig): (string | number)[];
@@ -0,0 +1,112 @@
1
+ import { useState, useTransition, useRef, useEffect } from "react";
2
+ import debounce from "lodash-es/debounce";
3
+ import { useMap } from "react-leaflet";
4
+ import { u as useMapStore } from "../useMapStore/index.js";
5
+ import { deepShallow } from "@m4l/components";
6
+ import { U as USE_FILTER_DECIMATION_DEFAULT_DEBOUNCE_MS } from "./constants.js";
7
+ function useFilterDecimation(featureIds, layerId, decimationFilter, config) {
8
+ const map = useMap();
9
+ const debounceMs = config?.debounceMs ?? USE_FILTER_DECIMATION_DEFAULT_DEBOUNCE_MS;
10
+ const [filteredIds, setFilteredIds] = useState(() => featureIds);
11
+ const [, startTransition] = useTransition();
12
+ const isMountedRef = useRef(true);
13
+ const featureIdsRef = useRef(featureIds);
14
+ featureIdsRef.current = featureIds;
15
+ const featuresWithCoords = useMapStore(
16
+ (state) => {
17
+ const geoJsonObject = state.hashLayers[layerId]?.geoJsonObject;
18
+ if (!geoJsonObject || geoJsonObject.object.type !== "FeatureCollection") {
19
+ return [];
20
+ }
21
+ const idSet = new Set(featureIds);
22
+ return geoJsonObject.object.features.filter((f) => idSet.has(f.id)).map((f) => {
23
+ if (f.geometry.type === "Point") {
24
+ const [lng, lat] = f.geometry.coordinates;
25
+ return { id: f.id, lat, lng };
26
+ }
27
+ return null;
28
+ }).filter((f) => f !== null);
29
+ },
30
+ deepShallow
31
+ );
32
+ useEffect(() => {
33
+ isMountedRef.current = true;
34
+ return () => {
35
+ isMountedRef.current = false;
36
+ };
37
+ }, []);
38
+ const cacheRef = useRef(/* @__PURE__ */ new Map());
39
+ useEffect(() => {
40
+ cacheRef.current.clear();
41
+ if (!decimationFilter) {
42
+ startTransition(() => setFilteredIds([...featureIds]));
43
+ return;
44
+ }
45
+ if (featureIds.length === 0) {
46
+ startTransition(() => setFilteredIds([]));
47
+ return;
48
+ }
49
+ if (!map) {
50
+ return;
51
+ }
52
+ const cache = {
53
+ /**
54
+ * Obtiene un valor cacheado o lo computa con factory y lo cachea.
55
+ */
56
+ get(key, factory) {
57
+ if (!cacheRef.current.has(key)) {
58
+ cacheRef.current.set(key, factory());
59
+ }
60
+ return cacheRef.current.get(key);
61
+ }
62
+ };
63
+ const computeFiltered = () => {
64
+ const currentFeatureIds = featureIdsRef.current;
65
+ if (currentFeatureIds.length === 0) {
66
+ if (isMountedRef.current) {
67
+ startTransition(() => setFilteredIds([]));
68
+ }
69
+ return;
70
+ }
71
+ const bounds = map.getBounds();
72
+ const zoom = map.getZoom();
73
+ const filtered = decimationFilter({
74
+ featureIds: currentFeatureIds,
75
+ layerId,
76
+ bounds,
77
+ zoom,
78
+ map,
79
+ featuresWithCoords,
80
+ cache
81
+ });
82
+ if (isMountedRef.current) {
83
+ startTransition(() => setFilteredIds(filtered));
84
+ }
85
+ };
86
+ const scheduleCompute = debounceMs <= 0 ? computeFiltered : debounce(computeFiltered, debounceMs);
87
+ computeFiltered();
88
+ map.on("zoomend", scheduleCompute);
89
+ map.on("moveend", scheduleCompute);
90
+ return () => {
91
+ const debouncedFn = scheduleCompute;
92
+ debouncedFn.cancel?.();
93
+ map.off("zoomend", scheduleCompute);
94
+ map.off("moveend", scheduleCompute);
95
+ };
96
+ }, [
97
+ decimationFilter,
98
+ debounceMs,
99
+ featureIds,
100
+ featuresWithCoords,
101
+ layerId,
102
+ map,
103
+ startTransition
104
+ ]);
105
+ if (!decimationFilter) {
106
+ return featureIds;
107
+ }
108
+ return filteredIds;
109
+ }
110
+ export {
111
+ useFilterDecimation as u
112
+ };
@@ -0,0 +1,5 @@
1
+ import { ZoomBasedFilterConfig, GridCell } from './types';
2
+ /**
3
+ * Hook que retorna información de la grilla para visualización (solo para debug)
4
+ */
5
+ export declare function useGridDebug(featureIds: (string | number)[], layerId: string, config?: ZoomBasedFilterConfig): GridCell[];