zenit-sdk 0.1.3 → 0.1.5

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.
@@ -1,4077 +0,0 @@
1
- // src/react/ZenitMap.tsx
2
- import React4, { useCallback as useCallback2, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo2, useRef as useRef5, useState as useState2, forwardRef } from "react";
3
- import { MapContainer, Marker as Marker2, TileLayer, ZoomControl } from "react-leaflet";
4
- import L4 from "leaflet";
5
-
6
- // src/maps/helpers.ts
7
- function toNumber(value) {
8
- if (typeof value === "number" && Number.isFinite(value)) {
9
- return value;
10
- }
11
- if (typeof value === "string") {
12
- const parsed = parseFloat(value);
13
- if (Number.isFinite(parsed)) {
14
- return parsed;
15
- }
16
- }
17
- return void 0;
18
- }
19
- function clampOpacity(value) {
20
- if (!Number.isFinite(value ?? NaN)) {
21
- return 1;
22
- }
23
- return Math.min(1, Math.max(0, value ?? 1));
24
- }
25
- function resolveLayerId(mapLayer) {
26
- if (mapLayer.layerId !== void 0 && mapLayer.layerId !== null) {
27
- return mapLayer.layerId;
28
- }
29
- const embedded = mapLayer.layer;
30
- return embedded?.id;
31
- }
32
- function extractMapDto(map) {
33
- if (map && typeof map === "object" && "data" in map) {
34
- const data = map.data;
35
- return data ?? null;
36
- }
37
- if (map && typeof map === "object") {
38
- return map;
39
- }
40
- return null;
41
- }
42
- function normalizeMapCenter(map) {
43
- const center = map.settings?.center;
44
- if (Array.isArray(center) && center.length >= 2) {
45
- const [lon, lat] = center;
46
- return {
47
- ...map,
48
- settings: {
49
- ...map.settings,
50
- center: { lon, lat }
51
- }
52
- };
53
- }
54
- return map;
55
- }
56
- function normalizeMapLayers(mapOrResponse) {
57
- const map = extractMapDto(mapOrResponse);
58
- if (!map?.mapLayers) {
59
- return [];
60
- }
61
- const normalized = [];
62
- map.mapLayers.forEach((mapLayer, index) => {
63
- const layerId = resolveLayerId(mapLayer);
64
- if (layerId === void 0) {
65
- return;
66
- }
67
- const embeddedLayer = mapLayer.layer;
68
- const visible = mapLayer.isVisible !== false;
69
- const opacity = clampOpacity(toNumber(mapLayer.opacity) ?? 1);
70
- const layerType = embeddedLayer?.layerType ?? mapLayer.layerType;
71
- const geometryType = embeddedLayer?.geometryType ?? mapLayer.geometryType;
72
- const layerName = embeddedLayer?.name ?? mapLayer.name;
73
- const orderCandidate = toNumber(mapLayer.displayOrder) ?? toNumber(mapLayer.order) ?? toNumber(embeddedLayer?.displayOrder) ?? index;
74
- normalized.push({
75
- layerId,
76
- displayOrder: orderCandidate ?? index,
77
- isVisible: visible,
78
- opacity,
79
- layer: embeddedLayer,
80
- layerType,
81
- geometryType,
82
- layerName,
83
- mapLayer
84
- });
85
- });
86
- return normalized.sort((a, b) => a.displayOrder - b.displayOrder);
87
- }
88
- function computeBBoxFromFeature(feature) {
89
- if (!feature || typeof feature !== "object") return null;
90
- const geometry = feature.geometry;
91
- if (!geometry || !("coordinates" in geometry)) return null;
92
- const coords = [];
93
- const collect = (candidate) => {
94
- if (!Array.isArray(candidate)) return;
95
- if (candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") {
96
- coords.push([candidate[0], candidate[1]]);
97
- return;
98
- }
99
- candidate.forEach((entry) => collect(entry));
100
- };
101
- collect(geometry.coordinates);
102
- if (coords.length === 0) return null;
103
- const [firstLon, firstLat] = coords[0];
104
- const bbox = { minLon: firstLon, minLat: firstLat, maxLon: firstLon, maxLat: firstLat };
105
- coords.forEach(([lon, lat]) => {
106
- bbox.minLon = Math.min(bbox.minLon, lon);
107
- bbox.minLat = Math.min(bbox.minLat, lat);
108
- bbox.maxLon = Math.max(bbox.maxLon, lon);
109
- bbox.maxLat = Math.max(bbox.maxLat, lat);
110
- });
111
- return bbox;
112
- }
113
- function centroidFromBBox(bbox) {
114
- return [(bbox.minLat + bbox.maxLat) / 2, (bbox.minLon + bbox.maxLon) / 2];
115
- }
116
- function applyFilteredGeoJSONStrategy(options) {
117
- const layerControls = options.normalizedLayers.map((entry) => ({
118
- layerId: entry.layerId,
119
- visible: entry.isVisible,
120
- opacity: entry.opacity
121
- }));
122
- return {
123
- overlay: options.filteredGeojson,
124
- metadata: options.metadata,
125
- layerControls
126
- };
127
- }
128
-
129
- // src/engine/LayerStateEngine.ts
130
- function clampOpacity2(value) {
131
- if (typeof value === "number" && Number.isFinite(value)) {
132
- return Math.min(1, Math.max(0, value));
133
- }
134
- if (typeof value === "string") {
135
- const parsed = parseFloat(value);
136
- if (Number.isFinite(parsed)) {
137
- return Math.min(1, Math.max(0, parsed));
138
- }
139
- }
140
- return 1;
141
- }
142
- var resolveBaseVisibility = (_layer) => false;
143
- function resolveBaseOpacity(layer) {
144
- if ("opacity" in layer) {
145
- return clampOpacity2(layer.opacity);
146
- }
147
- return 1;
148
- }
149
- function toEffectiveState(base, override) {
150
- const overrideVisible = override?.overrideVisible ?? void 0;
151
- const overrideOpacity = override?.overrideOpacity ?? void 0;
152
- const opacitySource = overrideOpacity !== void 0 && overrideOpacity !== null ? overrideOpacity : base.baseOpacity;
153
- return {
154
- layerId: base.layerId,
155
- baseVisible: base.baseVisible,
156
- baseOpacity: base.baseOpacity,
157
- overrideVisible,
158
- overrideOpacity,
159
- visible: overrideVisible ?? base.baseVisible,
160
- opacity: clampOpacity2(opacitySource)
161
- };
162
- }
163
- function initLayerStates(mapLayers) {
164
- const bases = mapLayers.filter((layer) => layer.layerId !== void 0).map((layer) => ({
165
- layerId: layer.layerId,
166
- baseVisible: resolveBaseVisibility(layer),
167
- baseOpacity: resolveBaseOpacity(layer)
168
- }));
169
- return bases.map((base) => toEffectiveState(base));
170
- }
171
- function applyLayerOverrides(states, overrides) {
172
- const overrideMap = /* @__PURE__ */ new Map();
173
- overrides.forEach((entry) => overrideMap.set(entry.layerId, entry));
174
- return states.map((state) => {
175
- const nextOverride = overrideMap.get(state.layerId);
176
- const mergedOverrideVisible = nextOverride && "overrideVisible" in nextOverride && nextOverride.overrideVisible !== void 0 ? nextOverride.overrideVisible : state.overrideVisible;
177
- const mergedOverrideOpacity = nextOverride && "overrideOpacity" in nextOverride && nextOverride.overrideOpacity !== void 0 ? nextOverride.overrideOpacity : state.overrideOpacity;
178
- return toEffectiveState(state, {
179
- layerId: state.layerId,
180
- overrideVisible: mergedOverrideVisible ?? void 0,
181
- overrideOpacity: mergedOverrideOpacity ?? void 0
182
- });
183
- });
184
- }
185
- function resetOverrides(states) {
186
- return states.map((state) => toEffectiveState(state));
187
- }
188
-
189
- // src/react/layerStyleHelpers.ts
190
- function resolveLayerAccent(style) {
191
- if (!style) return null;
192
- return style.fillColor ?? style.color ?? null;
193
- }
194
- function getLayerColor(style, fallback = "#94a3b8") {
195
- return resolveLayerAccent(style) ?? fallback;
196
- }
197
- function getStyleByLayerId(layerId, mapLayers) {
198
- if (!mapLayers) return null;
199
- const match = mapLayers.find((ml) => String(ml.layerId) === String(layerId));
200
- return match?.style ?? null;
201
- }
202
- function getAccentByLayerId(layerId, mapLayers) {
203
- const style = getStyleByLayerId(layerId, mapLayers);
204
- return resolveLayerAccent(style);
205
- }
206
-
207
- // src/react/zoomOpacity.ts
208
- var DEFAULT_OPTIONS = {
209
- minZoom: 11,
210
- maxZoom: 15,
211
- minFactor: 0.3,
212
- maxFactor: 1,
213
- minOpacity: 0.1,
214
- maxOpacity: 0.92
215
- };
216
- function clampNumber(value, min, max) {
217
- return Math.min(max, Math.max(min, value));
218
- }
219
- function clampOpacity3(value) {
220
- return clampNumber(value, 0, 1);
221
- }
222
- function isPolygonLayer(layerType, geometryType) {
223
- const candidate = (layerType ?? geometryType ?? "").toLowerCase();
224
- return candidate === "polygon" || candidate === "multipolygon";
225
- }
226
- function getZoomOpacityFactor(zoom, options) {
227
- const settings = { ...DEFAULT_OPTIONS, ...options };
228
- const { minZoom, maxZoom, minFactor, maxFactor } = settings;
229
- if (maxZoom <= minZoom) return clampNumber(maxFactor, minFactor, maxFactor);
230
- const t = clampNumber((zoom - minZoom) / (maxZoom - minZoom), 0, 1);
231
- const factor = maxFactor + (minFactor - maxFactor) * t;
232
- return clampNumber(factor, minFactor, maxFactor);
233
- }
234
- function getLayerZoomOpacityFactor(zoom, layerType, geometryType, options) {
235
- if (!isPolygonLayer(layerType, geometryType)) return 1;
236
- return getZoomOpacityFactor(zoom, options);
237
- }
238
- function getEffectiveLayerOpacity(baseOpacity, zoom, layerType, geometryType, options) {
239
- if (!isPolygonLayer(layerType, geometryType)) {
240
- return clampOpacity3(baseOpacity);
241
- }
242
- const settings = { ...DEFAULT_OPTIONS, ...options };
243
- const factor = getZoomOpacityFactor(zoom, options);
244
- const effective = clampOpacity3(baseOpacity) * factor;
245
- return clampNumber(effective, settings.minOpacity, settings.maxOpacity);
246
- }
247
-
248
- // src/react/map/layer-geojson.tsx
249
- import { useEffect, useRef } from "react";
250
- import { GeoJSON } from "react-leaflet";
251
- import L from "leaflet";
252
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
253
- var POINT_GEOMETRY_TYPES = /* @__PURE__ */ new Set(["Point", "MultiPoint"]);
254
- function normalizeBboxFromData(data) {
255
- const bboxCandidate = data.bbox;
256
- if (!Array.isArray(bboxCandidate) || bboxCandidate.length < 4) return null;
257
- const [minLon, minLat, maxLon, maxLat] = bboxCandidate;
258
- const values = [minLon, minLat, maxLon, maxLat].map((value) => Number(value));
259
- if (values.every((value) => Number.isFinite(value))) {
260
- return values;
261
- }
262
- return null;
263
- }
264
- function buildIdsSample(features) {
265
- return features.slice(0, 10).map((feature) => {
266
- const typedFeature = feature;
267
- if (typedFeature.id !== void 0 && typedFeature.id !== null) {
268
- return String(typedFeature.id);
269
- }
270
- const featureId = typedFeature.properties?.featureId;
271
- return featureId !== void 0 && featureId !== null ? String(featureId) : "";
272
- }).join(",");
273
- }
274
- function getGeometryType(feature) {
275
- const t = feature?.geometry?.type;
276
- return typeof t === "string" ? t : null;
277
- }
278
- function isPointGeometry(feature) {
279
- const geometryType = getGeometryType(feature);
280
- return geometryType !== null && POINT_GEOMETRY_TYPES.has(geometryType);
281
- }
282
- function isNonPointGeometry(feature) {
283
- const geometryType = getGeometryType(feature);
284
- return geometryType !== null && !POINT_GEOMETRY_TYPES.has(geometryType);
285
- }
286
- function buildFeatureCollection(features) {
287
- return {
288
- type: "FeatureCollection",
289
- features
290
- };
291
- }
292
- var LayerGeoJson = ({
293
- layerId,
294
- data,
295
- baseOpacity,
296
- isMobile,
297
- panesReady,
298
- mapInstance,
299
- fillPaneName,
300
- pointsPaneName,
301
- layerType,
302
- styleFn,
303
- onEachFeature,
304
- onPolygonLabel
305
- }) => {
306
- const features = data.features ?? [];
307
- const fillFeatures = features.filter(isNonPointGeometry);
308
- const pointFeatures = features.filter(isPointGeometry);
309
- const dataVersionRef = useRef(0);
310
- const prevSignatureRef = useRef("");
311
- const firstId = features.length > 0 ? String(features[0]?.id ?? "") : "";
312
- const lastId = features.length > 0 ? String(features[features.length - 1]?.id ?? "") : "";
313
- const bbox = normalizeBboxFromData(data);
314
- const idsSample = buildIdsSample(features);
315
- const signature = bbox ? `${layerId}|${features.length}|${firstId}|${lastId}|${idsSample}|${bbox[0]}|${bbox[1]}|${bbox[2]}|${bbox[3]}` : `${layerId}|${features.length}|${firstId}|${lastId}|${idsSample}`;
316
- const signatureToken = signature.replace(/[^a-zA-Z0-9_-]/g, "_");
317
- if (prevSignatureRef.current !== signature) {
318
- dataVersionRef.current += 1;
319
- prevSignatureRef.current = signature;
320
- }
321
- const fillData = fillFeatures.length > 0 ? buildFeatureCollection(fillFeatures) : null;
322
- const pointsData = pointFeatures.length > 0 ? buildFeatureCollection(pointFeatures) : null;
323
- const clusterLayerRef = useRef(null);
324
- const canCluster = typeof L.markerClusterGroup === "function";
325
- useEffect(() => {
326
- if (!mapInstance || !panesReady || !pointsData || !canCluster) return;
327
- const markerClusterGroup = L.markerClusterGroup;
328
- const clusterLayer = clusterLayerRef.current ?? markerClusterGroup();
329
- clusterLayerRef.current = clusterLayer;
330
- if (!mapInstance.hasLayer(clusterLayer)) {
331
- mapInstance.addLayer(clusterLayer);
332
- }
333
- clusterLayer.clearLayers();
334
- const geoJsonLayer = L.geoJSON(pointsData, {
335
- pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
336
- radius: isMobile ? 8 : 6,
337
- pane: mapInstance.getPane(pointsPaneName) ? pointsPaneName : void 0,
338
- ...styleFn(feature, layerType, baseOpacity)
339
- }),
340
- onEachFeature
341
- });
342
- clusterLayer.addLayer(geoJsonLayer);
343
- return () => {
344
- clusterLayer.clearLayers();
345
- if (mapInstance.hasLayer(clusterLayer)) {
346
- mapInstance.removeLayer(clusterLayer);
347
- }
348
- };
349
- }, [
350
- baseOpacity,
351
- canCluster,
352
- isMobile,
353
- layerType,
354
- mapInstance,
355
- onEachFeature,
356
- panesReady,
357
- pointsData,
358
- pointsPaneName,
359
- styleFn
360
- ]);
361
- return /* @__PURE__ */ jsxs(Fragment, { children: [
362
- fillData && /* @__PURE__ */ jsx(
363
- GeoJSON,
364
- {
365
- data: fillData,
366
- pane: panesReady && mapInstance?.getPane(fillPaneName) ? fillPaneName : void 0,
367
- style: (feature) => styleFn(feature, layerType, baseOpacity),
368
- onEachFeature: (feature, layer) => {
369
- onEachFeature(feature, layer);
370
- onPolygonLabel?.(feature, layer);
371
- }
372
- },
373
- `fill-${layerId}-${signatureToken}-v${dataVersionRef.current}`
374
- ),
375
- pointsData && !canCluster && /* @__PURE__ */ jsx(
376
- GeoJSON,
377
- {
378
- data: pointsData,
379
- pane: panesReady && mapInstance?.getPane(pointsPaneName) ? pointsPaneName : void 0,
380
- pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
381
- radius: isMobile ? 8 : 6,
382
- ...styleFn(feature, layerType, baseOpacity)
383
- }),
384
- onEachFeature
385
- },
386
- `points-${layerId}-${signatureToken}-v${dataVersionRef.current}`
387
- )
388
- ] });
389
- };
390
-
391
- // src/react/map/location-control.tsx
392
- import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "react";
393
- import { createPortal } from "react-dom";
394
- import { Circle, Marker, useMap } from "react-leaflet";
395
- import L3 from "leaflet";
396
-
397
- // src/react/hooks/use-geolocation.ts
398
- import { useCallback, useEffect as useEffect2, useRef as useRef2, useState } from "react";
399
- function useGeolocation(options) {
400
- const [isTracking, setIsTracking] = useState(false);
401
- const [location, setLocation] = useState(null);
402
- const [error, setError] = useState(null);
403
- const watchIdRef = useRef2(null);
404
- const stopTracking = useCallback(() => {
405
- if (watchIdRef.current !== null && typeof navigator !== "undefined" && navigator.geolocation) {
406
- navigator.geolocation.clearWatch(watchIdRef.current);
407
- }
408
- watchIdRef.current = null;
409
- setIsTracking(false);
410
- }, []);
411
- const startTracking = useCallback(() => {
412
- if (typeof navigator === "undefined" || !navigator.geolocation) {
413
- setError({ code: 0, message: "La geolocalizaci\xF3n no est\xE1 disponible en este navegador." });
414
- return;
415
- }
416
- setError(null);
417
- watchIdRef.current = navigator.geolocation.watchPosition(
418
- (position) => {
419
- setLocation({
420
- lat: position.coords.latitude,
421
- lon: position.coords.longitude,
422
- accuracy: position.coords.accuracy
423
- });
424
- setIsTracking(true);
425
- },
426
- (err) => {
427
- setError({ code: err.code, message: err.message });
428
- stopTracking();
429
- },
430
- {
431
- enableHighAccuracy: options?.enableHighAccuracy ?? true,
432
- timeout: options?.timeout ?? 12e3,
433
- maximumAge: options?.maximumAge ?? 0
434
- }
435
- );
436
- }, [options?.enableHighAccuracy, options?.maximumAge, options?.timeout, stopTracking]);
437
- const toggleTracking = useCallback(() => {
438
- if (isTracking) {
439
- stopTracking();
440
- } else {
441
- startTracking();
442
- }
443
- }, [isTracking, startTracking, stopTracking]);
444
- const clearError = useCallback(() => setError(null), []);
445
- useEffect2(() => {
446
- return () => {
447
- stopTracking();
448
- };
449
- }, [stopTracking]);
450
- return {
451
- isTracking,
452
- location,
453
- error,
454
- startTracking,
455
- stopTracking,
456
- toggleTracking,
457
- clearError
458
- };
459
- }
460
-
461
- // src/react/map/map-utils.ts
462
- import L2 from "leaflet";
463
- var POPUP_STYLE_ID = "zenit-leaflet-popup-styles";
464
- var POPUP_EXCLUDED_KEYS = /* @__PURE__ */ new Set(["geom", "geometry", "_private"]);
465
- var POPUP_HEADER_KEYS = ["nombre", "name", "title", "titulo"];
466
- var DESKTOP_POPUP_DIMENSIONS = { maxWidth: 360, minWidth: 280, maxHeight: 520 };
467
- var MOBILE_POPUP_DIMENSIONS = { maxWidth: 300, minWidth: 240, maxHeight: 420 };
468
- var ZENIT_LEAFLET_POPUP_STYLES = `
469
- .custom-leaflet-popup .leaflet-popup-content-wrapper {
470
- border-radius: 12px;
471
- padding: 0;
472
- background: #ffffff;
473
- box-shadow: 0 12px 24px rgba(15, 23, 42, 0.18);
474
- border: 1px solid rgba(15, 23, 42, 0.08);
475
- }
476
-
477
- .custom-leaflet-popup .leaflet-popup-content {
478
- margin: 0;
479
- padding: 12px 14px;
480
- font-size: 13px;
481
- line-height: 1.4;
482
- color: #0f172a;
483
- max-height: min(70vh, 520px);
484
- overflow: auto;
485
- scrollbar-width: thin;
486
- scrollbar-color: rgba(148, 163, 184, 0.6) transparent;
487
- }
488
-
489
- .custom-leaflet-popup .leaflet-popup-close-button {
490
- color: #64748b;
491
- font-size: 16px;
492
- padding: 6px 8px;
493
- border-radius: 8px;
494
- transition: background-color 0.15s ease, color 0.15s ease;
495
- }
496
-
497
- .custom-leaflet-popup .leaflet-popup-close-button:hover {
498
- color: #0f172a;
499
- background: rgba(148, 163, 184, 0.2);
500
- }
501
-
502
- .custom-leaflet-popup .leaflet-popup-content::-webkit-scrollbar {
503
- width: 6px;
504
- }
505
-
506
- .custom-leaflet-popup .leaflet-popup-content::-webkit-scrollbar-thumb {
507
- background: rgba(148, 163, 184, 0.5);
508
- border-radius: 999px;
509
- }
510
-
511
- .custom-leaflet-popup .leaflet-popup-content::-webkit-scrollbar-track {
512
- background: transparent;
513
- }
514
-
515
- @media (max-width: 640px) {
516
- .custom-leaflet-popup .leaflet-popup-content {
517
- font-size: 12px;
518
- padding: 10px 12px;
519
- max-height: min(65vh, 420px);
520
- }
521
-
522
- .custom-leaflet-popup .leaflet-popup-close-button {
523
- font-size: 14px;
524
- padding: 4px 6px;
525
- }
526
- }
527
-
528
- .zenit-map-tooltip {
529
- background-color: rgba(31, 41, 55, 0.95);
530
- border: none;
531
- border-radius: 6px;
532
- color: #ffffff;
533
- font-size: 12px;
534
- font-weight: 500;
535
- padding: 6px 10px;
536
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
537
- }
538
-
539
- .zenit-map-tooltip::before {
540
- border-top-color: rgba(31, 41, 55, 0.95);
541
- }
542
-
543
- .polygon-label-tooltip {
544
- background: rgba(15, 23, 42, 0.92);
545
- border: none;
546
- border-radius: 6px;
547
- color: #f8fafc;
548
- font-weight: 600;
549
- font-size: 12px;
550
- padding: 4px 8px;
551
- }
552
- `;
553
- function clampNumber2(value, min, max) {
554
- return Math.min(max, Math.max(min, value));
555
- }
556
- function clampOpacity4(value) {
557
- return clampNumber2(value, 0, 1);
558
- }
559
- function ensurePopupStyles() {
560
- if (typeof document === "undefined") return;
561
- if (document.getElementById(POPUP_STYLE_ID)) return;
562
- const styleTag = document.createElement("style");
563
- styleTag.id = POPUP_STYLE_ID;
564
- styleTag.textContent = ZENIT_LEAFLET_POPUP_STYLES;
565
- document.head.appendChild(styleTag);
566
- }
567
- function getPopupDimensions() {
568
- if (typeof window === "undefined") {
569
- return DESKTOP_POPUP_DIMENSIONS;
570
- }
571
- const isSmallWidth = window.innerWidth < 640;
572
- const isShortHeight = window.innerHeight < 768;
573
- const base = isSmallWidth ? MOBILE_POPUP_DIMENSIONS : DESKTOP_POPUP_DIMENSIONS;
574
- const maxHeight = isShortHeight ? Math.min(base.maxHeight, 360) : base.maxHeight;
575
- return { ...base, maxHeight };
576
- }
577
- function escapeHtml(value) {
578
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
579
- }
580
- function formatLabel(key) {
581
- return key.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
582
- }
583
- function renderPopupValue(value) {
584
- if (value === null || value === void 0) return '<span style="color:#94a3b8;">Sin datos</span>';
585
- if (value instanceof Date) {
586
- return escapeHtml(value.toLocaleDateString("es-GT"));
587
- }
588
- if (typeof value === "number") {
589
- return escapeHtml(value.toLocaleString("es-GT"));
590
- }
591
- if (typeof value === "string") {
592
- const trimmed = value.trim();
593
- if (!trimmed) return '<span style="color:#94a3b8;">Sin datos</span>';
594
- return escapeHtml(trimmed);
595
- }
596
- if (typeof value === "object") {
597
- try {
598
- return `<pre style="margin:0; white-space:pre-wrap; font-size:11px; background:#f8fafc; border:1px solid #e2e8f0; padding:6px 8px; border-radius:6px;">${escapeHtml(
599
- JSON.stringify(value, null, 2)
600
- )}</pre>`;
601
- } catch {
602
- return escapeHtml(String(value));
603
- }
604
- }
605
- return escapeHtml(String(value));
606
- }
607
- function extractPopupHeader(properties) {
608
- const entry = Object.entries(properties).find(([key, value]) => {
609
- if (typeof value !== "string") return false;
610
- const normalized = key.trim().toLowerCase();
611
- return POPUP_HEADER_KEYS.includes(normalized) && value.trim().length > 0;
612
- });
613
- return entry ? entry[1].trim() : null;
614
- }
615
- function shouldIncludePopupEntry(key, value) {
616
- if (!key) return false;
617
- const normalized = key.trim().toLowerCase();
618
- if (!normalized) return false;
619
- if (normalized.startsWith("_")) return false;
620
- if (POPUP_EXCLUDED_KEYS.has(normalized)) return false;
621
- if (value === null || value === void 0) return false;
622
- if (typeof value === "string" && !value.trim()) return false;
623
- return true;
624
- }
625
- function createPopupContent(properties) {
626
- const headerText = extractPopupHeader(properties);
627
- const entries = Object.entries(properties).filter(([key, value]) => {
628
- if (!shouldIncludePopupEntry(key, value)) return false;
629
- if (headerText && POPUP_HEADER_KEYS.includes(key.trim().toLowerCase())) return false;
630
- return true;
631
- });
632
- if (entries.length === 0) {
633
- return '<div style="padding:8px 0; color:#64748b; text-align:center;">Sin datos disponibles</div>';
634
- }
635
- const headerHtml = headerText ? `<div style="font-weight:700; font-size:14px; margin-bottom:8px; color:#0f172a;">${escapeHtml(
636
- headerText
637
- )}</div>` : "";
638
- const rowsHtml = entries.map(([key, value]) => {
639
- const label = escapeHtml(formatLabel(key));
640
- const valueHtml = renderPopupValue(value);
641
- return `
642
- <div style="display:grid; grid-template-columns:minmax(90px, 35%) 1fr; gap:8px; padding:6px 0; border-bottom:1px solid #e2e8f0;">
643
- <div style="font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.04em; color:#64748b;">${label}</div>
644
- <div style="font-size:13px; color:#0f172a; word-break:break-word;">${valueHtml}</div>
645
- </div>
646
- `;
647
- }).join("");
648
- return `<div>${headerHtml}${rowsHtml}</div>`;
649
- }
650
- function isPolygonType(layerType, geometryType) {
651
- const candidate = (layerType ?? geometryType ?? "").toLowerCase();
652
- return candidate === "polygon" || candidate === "multipolygon";
653
- }
654
- function calculateZoomBasedOpacity(zoom, baseOpacity, layerType, geometryType) {
655
- if (!isPolygonType(layerType, geometryType)) return clampOpacity4(baseOpacity);
656
- const minZoom = 11;
657
- const maxZoom = 15;
658
- const minFactor = 0.3;
659
- if (maxZoom <= minZoom) return clampOpacity4(baseOpacity * minFactor);
660
- const t = clampNumber2((zoom - minZoom) / (maxZoom - minZoom), 0, 1);
661
- const factor = 1 - (1 - minFactor) * t;
662
- return clampOpacity4(baseOpacity * factor);
663
- }
664
- function layerStyleToLeaflet(options) {
665
- const { baseOpacity, zoom, layerStyle, geometryType, layerType } = options;
666
- const sanitizedOpacity = clampOpacity4(baseOpacity);
667
- const zoomOpacity = calculateZoomBasedOpacity(zoom, sanitizedOpacity, layerType, geometryType);
668
- const styleFillOpacity = typeof layerStyle?.fillOpacity === "number" ? clampOpacity4(layerStyle.fillOpacity) : 0.8;
669
- return {
670
- color: layerStyle?.color ?? layerStyle?.fillColor ?? "#2563eb",
671
- weight: layerStyle?.weight ?? 2,
672
- fillColor: layerStyle?.fillColor ?? layerStyle?.color ?? "#2563eb",
673
- opacity: clampOpacity4(Math.max(0.35, zoomOpacity * 0.9)),
674
- fillOpacity: clampOpacity4(zoomOpacity * styleFillOpacity)
675
- };
676
- }
677
- function getRgbFromColor(color) {
678
- const trimmed = color.trim();
679
- if (trimmed.startsWith("#")) {
680
- const hex = trimmed.replace("#", "");
681
- const expanded = hex.length === 3 ? hex.split("").map((char) => `${char}${char}`).join("") : hex;
682
- if (expanded.length === 6) {
683
- const r = parseInt(expanded.slice(0, 2), 16);
684
- const g = parseInt(expanded.slice(2, 4), 16);
685
- const b = parseInt(expanded.slice(4, 6), 16);
686
- return { r, g, b };
687
- }
688
- }
689
- const rgbMatch = trimmed.match(/rgba?\(([^)]+)\)/i);
690
- if (rgbMatch) {
691
- const [r, g, b] = rgbMatch[1].split(",").map((value) => parseFloat(value.trim())).slice(0, 3);
692
- if ([r, g, b].every((value) => Number.isFinite(value))) {
693
- return { r, g, b };
694
- }
695
- }
696
- return null;
697
- }
698
- function getLabelTextStyles(color) {
699
- const rgb = getRgbFromColor(color);
700
- if (!rgb) {
701
- return { color: "#0f172a", shadow: "0 1px 2px rgba(255, 255, 255, 0.6)" };
702
- }
703
- const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
704
- if (luminance > 0.6) {
705
- return { color: "#0f172a", shadow: "0 1px 2px rgba(255, 255, 255, 0.7)" };
706
- }
707
- return { color: "#ffffff", shadow: "0 1px 2px rgba(0, 0, 0, 0.4)" };
708
- }
709
- function withAlpha(color, alpha) {
710
- const trimmed = color.trim();
711
- if (trimmed.startsWith("#")) {
712
- const hex = trimmed.replace("#", "");
713
- const expanded = hex.length === 3 ? hex.split("").map((char) => `${char}${char}`).join("") : hex;
714
- if (expanded.length === 6) {
715
- const r = parseInt(expanded.slice(0, 2), 16);
716
- const g = parseInt(expanded.slice(2, 4), 16);
717
- const b = parseInt(expanded.slice(4, 6), 16);
718
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
719
- }
720
- }
721
- if (trimmed.startsWith("rgb(")) {
722
- const inner = trimmed.slice(4, -1);
723
- return `rgba(${inner}, ${alpha})`;
724
- }
725
- if (trimmed.startsWith("rgba(")) {
726
- const inner = trimmed.slice(5, -1).split(",").slice(0, 3).map((value) => value.trim());
727
- return `rgba(${inner.join(", ")}, ${alpha})`;
728
- }
729
- return color;
730
- }
731
- function createCustomIcon(label, opacity, color) {
732
- const size = 60;
733
- const innerSize = 44;
734
- const textStyles = getLabelTextStyles(color);
735
- const safeLabel = escapeHtml(label);
736
- const clampedOpacity = Math.min(1, Math.max(0.92, opacity));
737
- const innerBackground = withAlpha(color, 0.9);
738
- return L2.divIcon({
739
- className: "zenit-label-marker",
740
- iconSize: [size, size],
741
- iconAnchor: [size / 2, size / 2],
742
- html: `
743
- <div
744
- title="${safeLabel}"
745
- style="
746
- width:${size}px;
747
- height:${size}px;
748
- border-radius:9999px;
749
- background:rgba(255, 255, 255, 0.95);
750
- border:3px solid rgba(255, 255, 255, 1);
751
- display:flex;
752
- align-items:center;
753
- justify-content:center;
754
- opacity:${clampedOpacity};
755
- box-shadow:0 2px 6px rgba(0, 0, 0, 0.25);
756
- pointer-events:none;
757
- "
758
- >
759
- <div
760
- style="
761
- width:${innerSize}px;
762
- height:${innerSize}px;
763
- border-radius:9999px;
764
- background:${innerBackground};
765
- display:flex;
766
- align-items:center;
767
- justify-content:center;
768
- box-shadow:inset 0 0 0 1px rgba(15, 23, 42, 0.12);
769
- "
770
- >
771
- <span
772
- style="
773
- color:${textStyles.color};
774
- font-size:20px;
775
- font-weight:800;
776
- text-shadow:${textStyles.shadow};
777
- "
778
- >
779
- ${safeLabel}
780
- </span>
781
- </div>
782
- </div>
783
- `
784
- });
785
- }
786
- function createLocationIcon() {
787
- return L2.divIcon({
788
- className: "zenit-location-marker",
789
- iconSize: [18, 18],
790
- iconAnchor: [9, 9],
791
- html: `
792
- <div style="width:18px;height:18px;border-radius:9999px;background:#2563eb;border:2px solid #fff;box-shadow:0 0 0 4px rgba(37, 99, 235, 0.25);"></div>
793
- `
794
- });
795
- }
796
-
797
- // src/react/map/location-control.tsx
798
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
799
- var LOCATION_STYLE_ID = "zenit-location-control-styles";
800
- var LOCATION_STYLES = `
801
- .zenit-location-control {
802
- display: flex;
803
- flex-direction: column;
804
- gap: 8px;
805
- }
806
-
807
- .zenit-location-button {
808
- width: 42px;
809
- height: 42px;
810
- border-radius: 12px;
811
- border: none;
812
- background: #ffffff;
813
- color: #0f172a;
814
- box-shadow: 0 8px 18px rgba(15, 23, 42, 0.2);
815
- display: inline-flex;
816
- align-items: center;
817
- justify-content: center;
818
- cursor: pointer;
819
- position: relative;
820
- }
821
-
822
- .zenit-location-button.zenit-location-button--tracking {
823
- animation: zenitLocationPulse 1.8s infinite;
824
- }
825
-
826
- .zenit-location-button:hover {
827
- background: #f8fafc;
828
- }
829
-
830
- .zenit-location-badge {
831
- position: absolute;
832
- top: -6px;
833
- right: -6px;
834
- width: 18px;
835
- height: 18px;
836
- border-radius: 999px;
837
- background: #ef4444;
838
- color: #fff;
839
- font-size: 11px;
840
- font-weight: 700;
841
- display: inline-flex;
842
- align-items: center;
843
- justify-content: center;
844
- box-shadow: 0 4px 10px rgba(239, 68, 68, 0.35);
845
- }
846
-
847
- .zenit-location-error {
848
- background: rgba(15, 23, 42, 0.92);
849
- color: #f8fafc;
850
- border-radius: 10px;
851
- padding: 8px 10px;
852
- font-size: 12px;
853
- max-width: 200px;
854
- box-shadow: 0 10px 24px rgba(15, 23, 42, 0.3);
855
- }
856
-
857
- @keyframes zenitLocationPulse {
858
- 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.35); }
859
- 70% { box-shadow: 0 0 0 12px rgba(16, 185, 129, 0); }
860
- 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
861
- }
862
- `;
863
- var LocateIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
864
- /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "3" }),
865
- /* @__PURE__ */ jsx2("line", { x1: "12", y1: "2", x2: "12", y2: "5" }),
866
- /* @__PURE__ */ jsx2("line", { x1: "12", y1: "19", x2: "12", y2: "22" }),
867
- /* @__PURE__ */ jsx2("line", { x1: "2", y1: "12", x2: "5", y2: "12" }),
868
- /* @__PURE__ */ jsx2("line", { x1: "19", y1: "12", x2: "22", y2: "12" })
869
- ] });
870
- var LocationControl = ({ position = "bottomleft", zoom = 16 }) => {
871
- const map = useMap();
872
- const controlRef = useRef3(null);
873
- const hasCenteredRef = useRef3(false);
874
- const { isTracking, location, error, toggleTracking, clearError } = useGeolocation();
875
- useEffect3(() => {
876
- if (typeof document === "undefined") return;
877
- if (document.getElementById(LOCATION_STYLE_ID)) return;
878
- const styleTag = document.createElement("style");
879
- styleTag.id = LOCATION_STYLE_ID;
880
- styleTag.textContent = LOCATION_STYLES;
881
- document.head.appendChild(styleTag);
882
- }, []);
883
- useEffect3(() => {
884
- const control = L3.control({ position });
885
- control.onAdd = () => {
886
- const container = L3.DomUtil.create("div", "zenit-location-control");
887
- L3.DomEvent.disableClickPropagation(container);
888
- controlRef.current = container;
889
- return container;
890
- };
891
- control.addTo(map);
892
- return () => {
893
- control.remove();
894
- controlRef.current = null;
895
- };
896
- }, [map, position]);
897
- useEffect3(() => {
898
- if (!location || !isTracking) return;
899
- if (hasCenteredRef.current) return;
900
- hasCenteredRef.current = true;
901
- map.flyTo([location.lat, location.lon], zoom, { animate: true });
902
- }, [isTracking, location, map, zoom]);
903
- const markerIcon = useMemo(() => createLocationIcon(), []);
904
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
905
- controlRef.current && createPortal(
906
- /* @__PURE__ */ jsxs2("div", { children: [
907
- /* @__PURE__ */ jsxs2(
908
- "button",
909
- {
910
- type: "button",
911
- className: `zenit-location-button${isTracking ? " zenit-location-button--tracking" : ""}`,
912
- onClick: () => {
913
- if (error) {
914
- clearError();
915
- }
916
- toggleTracking();
917
- },
918
- "aria-label": isTracking ? "Detener ubicaci\xF3n" : "Mostrar mi ubicaci\xF3n",
919
- children: [
920
- /* @__PURE__ */ jsx2(LocateIcon, {}),
921
- error && /* @__PURE__ */ jsx2("span", { className: "zenit-location-badge", children: "!" })
922
- ]
923
- }
924
- ),
925
- error && /* @__PURE__ */ jsx2("div", { className: "zenit-location-error", children: error.message || "No se pudo acceder a tu ubicaci\xF3n." })
926
- ] }),
927
- controlRef.current
928
- ),
929
- location && /* @__PURE__ */ jsxs2(Fragment2, { children: [
930
- /* @__PURE__ */ jsx2(Marker, { position: [location.lat, location.lon], icon: markerIcon }),
931
- /* @__PURE__ */ jsx2(
932
- Circle,
933
- {
934
- center: [location.lat, location.lon],
935
- radius: location.accuracy,
936
- pathOptions: { color: "#2563eb", fillColor: "#2563eb", fillOpacity: 0.15 }
937
- }
938
- )
939
- ] })
940
- ] });
941
- };
942
-
943
- // src/react/map/map-handlers.tsx
944
- import { useEffect as useEffect4, useRef as useRef4 } from "react";
945
- import { useMap as useMap2 } from "react-leaflet";
946
- var MapInvalidator = ({ trigger }) => {
947
- const map = useMap2();
948
- const lastTrigger = useRef4(void 0);
949
- useEffect4(() => {
950
- if (lastTrigger.current === trigger) return;
951
- lastTrigger.current = trigger;
952
- requestAnimationFrame(() => {
953
- map.invalidateSize();
954
- });
955
- }, [map, trigger]);
956
- return null;
957
- };
958
- function computeBBoxFromGeojson(geojson) {
959
- if (!geojson || !Array.isArray(geojson.features)) return null;
960
- const coords = [];
961
- const collect = (candidate) => {
962
- if (!Array.isArray(candidate)) return;
963
- if (candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number" && Number.isFinite(candidate[0]) && Number.isFinite(candidate[1])) {
964
- coords.push([candidate[0], candidate[1]]);
965
- return;
966
- }
967
- candidate.forEach((item) => collect(item));
968
- };
969
- geojson.features.forEach((feature) => {
970
- collect(feature.geometry?.coordinates);
971
- });
972
- if (coords.length === 0) return null;
973
- const [firstLon, firstLat] = coords[0];
974
- const bbox = { minLon: firstLon, minLat: firstLat, maxLon: firstLon, maxLat: firstLat };
975
- coords.forEach(([lon, lat]) => {
976
- bbox.minLon = Math.min(bbox.minLon, lon);
977
- bbox.minLat = Math.min(bbox.minLat, lat);
978
- bbox.maxLon = Math.max(bbox.maxLon, lon);
979
- bbox.maxLat = Math.max(bbox.maxLat, lat);
980
- });
981
- return bbox;
982
- }
983
- function mergeBBoxes(bboxes) {
984
- const valid = bboxes.filter((bbox) => !!bbox);
985
- if (valid.length === 0) return null;
986
- const first = valid[0];
987
- return valid.slice(1).reduce(
988
- (acc, bbox) => ({
989
- minLon: Math.min(acc.minLon, bbox.minLon),
990
- minLat: Math.min(acc.minLat, bbox.minLat),
991
- maxLon: Math.max(acc.maxLon, bbox.maxLon),
992
- maxLat: Math.max(acc.maxLat, bbox.maxLat)
993
- }),
994
- { ...first }
995
- );
996
- }
997
- var BBoxZoomHandler = ({
998
- bbox,
999
- geojson,
1000
- autoGeojson = [],
1001
- enabled = true
1002
- }) => {
1003
- const map = useMap2();
1004
- const lastAppliedBBox = useRef4(null);
1005
- const lastUserInteracted = useRef4(false);
1006
- useEffect4(() => {
1007
- const handleInteraction = () => {
1008
- lastUserInteracted.current = true;
1009
- };
1010
- map.on("dragstart", handleInteraction);
1011
- map.on("zoomstart", handleInteraction);
1012
- return () => {
1013
- map.off("dragstart", handleInteraction);
1014
- map.off("zoomstart", handleInteraction);
1015
- };
1016
- }, [map]);
1017
- useEffect4(() => {
1018
- if (!enabled) return;
1019
- let resolvedBBox = bbox ?? null;
1020
- if (!resolvedBBox && geojson) {
1021
- resolvedBBox = computeBBoxFromGeojson(geojson);
1022
- }
1023
- if (!resolvedBBox && autoGeojson.length > 0) {
1024
- const bboxes = autoGeojson.map((collection) => computeBBoxFromGeojson(collection));
1025
- resolvedBBox = mergeBBoxes(bboxes);
1026
- }
1027
- if (!resolvedBBox) return;
1028
- const serialized = JSON.stringify(resolvedBBox);
1029
- if (lastAppliedBBox.current === serialized) return;
1030
- if (lastUserInteracted.current && !bbox && !geojson) {
1031
- lastUserInteracted.current = false;
1032
- return;
1033
- }
1034
- const bounds = [
1035
- [resolvedBBox.minLat, resolvedBBox.minLon],
1036
- [resolvedBBox.maxLat, resolvedBBox.maxLon]
1037
- ];
1038
- map.fitBounds(bounds, { padding: [12, 12] });
1039
- lastAppliedBBox.current = serialized;
1040
- }, [autoGeojson, bbox, enabled, geojson, map]);
1041
- return null;
1042
- };
1043
- var ZoomBasedOpacityHandler = ({ onZoomChange }) => {
1044
- const map = useMap2();
1045
- useEffect4(() => {
1046
- const handleZoom = () => {
1047
- onZoomChange(map.getZoom());
1048
- };
1049
- map.on("zoomend", handleZoom);
1050
- handleZoom();
1051
- return () => {
1052
- map.off("zoomend", handleZoom);
1053
- };
1054
- }, [map, onZoomChange]);
1055
- return null;
1056
- };
1057
- var MapInstanceBridge = ({ onReady }) => {
1058
- const map = useMap2();
1059
- useEffect4(() => {
1060
- onReady(map);
1061
- }, [map, onReady]);
1062
- return null;
1063
- };
1064
-
1065
- // src/react/ZenitMap.tsx
1066
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1067
- var DEFAULT_CENTER = [0, 0];
1068
- var DEFAULT_ZOOM = 3;
1069
- var LABELS_PANE_NAME = "zenit-labels-pane";
1070
- function isRecord(value) {
1071
- return typeof value === "object" && value !== null;
1072
- }
1073
- function isGeoJsonFeatureCollection(value) {
1074
- if (!isRecord(value)) return false;
1075
- const features = value.features;
1076
- if (!Array.isArray(features)) return false;
1077
- const type = value.type;
1078
- return type === void 0 || type === "FeatureCollection";
1079
- }
1080
- function extractGeoJsonFeatureCollection(value) {
1081
- if (isRecord(value) && "data" in value) {
1082
- const data = value.data;
1083
- return isGeoJsonFeatureCollection(data) ? data : null;
1084
- }
1085
- return isGeoJsonFeatureCollection(value) ? value : null;
1086
- }
1087
- function getFeatureLayerId(feature) {
1088
- const layerId = feature?.properties?.__zenit_layerId ?? feature?.properties?.layerId ?? feature?.properties?.layer_id;
1089
- if (layerId === void 0 || layerId === null) return null;
1090
- return layerId;
1091
- }
1092
- var DESCRIPTION_KEYS = /* @__PURE__ */ new Set(["descripcion", "description"]);
1093
- function normalizeDescriptionValue(value) {
1094
- if (value === void 0 || value === null) return null;
1095
- if (typeof value === "string") {
1096
- const trimmed = value.trim();
1097
- return trimmed ? trimmed : null;
1098
- }
1099
- if (typeof value === "number" || typeof value === "boolean") {
1100
- return String(value);
1101
- }
1102
- return null;
1103
- }
1104
- function extractDescriptionValue(properties) {
1105
- if (!properties) return null;
1106
- const matches = Object.entries(properties).find(
1107
- ([key]) => DESCRIPTION_KEYS.has(key.toLowerCase())
1108
- );
1109
- if (!matches) return null;
1110
- return normalizeDescriptionValue(matches[1]);
1111
- }
1112
- function getFeatureStyleOverrides(feature) {
1113
- const candidate = feature?.properties?._style;
1114
- if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return null;
1115
- const styleCandidate = candidate;
1116
- return {
1117
- color: styleCandidate.color,
1118
- weight: styleCandidate.weight,
1119
- fillColor: styleCandidate.fillColor,
1120
- fillOpacity: styleCandidate.fillOpacity
1121
- };
1122
- }
1123
- function buildFeaturePopupHtml(feature) {
1124
- const properties = feature?.properties;
1125
- if (!properties) return null;
1126
- const rendered = createPopupContent(properties);
1127
- return rendered ? rendered : null;
1128
- }
1129
- function pickIntersectFeature(baseFeature, candidates) {
1130
- if (!Array.isArray(candidates) || candidates.length === 0) return null;
1131
- const baseId = baseFeature?.id;
1132
- if (baseId !== void 0 && baseId !== null) {
1133
- const matchById = candidates.find((candidate) => candidate?.id === baseId);
1134
- if (matchById) return matchById;
1135
- }
1136
- const matchWithDescription = candidates.find(
1137
- (candidate) => extractDescriptionValue(candidate?.properties)
1138
- );
1139
- return matchWithDescription ?? candidates[0];
1140
- }
1141
- function normalizeCenterTuple(center) {
1142
- if (!center) return null;
1143
- if (Array.isArray(center) && center.length >= 2) {
1144
- const lat = center[0];
1145
- const lon = center[1];
1146
- if (typeof lat === "number" && typeof lon === "number") return [lat, lon];
1147
- return null;
1148
- }
1149
- const maybeObj = center;
1150
- if (typeof maybeObj.lat === "number" && typeof maybeObj.lon === "number") {
1151
- return [maybeObj.lat, maybeObj.lon];
1152
- }
1153
- return null;
1154
- }
1155
- var ZenitMap = forwardRef(({
1156
- client,
1157
- mapId,
1158
- height = "500px",
1159
- width = "100%",
1160
- initialZoom,
1161
- initialCenter,
1162
- showLayerPanel = true,
1163
- overlayGeojson,
1164
- overlayStyle,
1165
- layerControls,
1166
- layerStates,
1167
- layerGeojson,
1168
- onLayerStateChange,
1169
- onError,
1170
- onLoadingChange,
1171
- onFeatureClick,
1172
- onFeatureHover,
1173
- featureInfoMode = "popup",
1174
- mapLayers,
1175
- zoomToBbox,
1176
- zoomToGeojson,
1177
- onZoomChange,
1178
- onMapReady
1179
- }, ref) => {
1180
- const [map, setMap] = useState2(null);
1181
- const [layers, setLayers] = useState2([]);
1182
- const [effectiveStates, setEffectiveStates] = useState2([]);
1183
- const [loadingMap, setLoadingMap] = useState2(false);
1184
- const [mapError, setMapError] = useState2(null);
1185
- const [mapInstance, setMapInstance] = useState2(null);
1186
- const [layerGeojsonOverrides, setLayerGeojsonOverrides] = useState2({});
1187
- const originalGeojsonByLayerIdRef = useRef5({});
1188
- const [panesReady, setPanesReady] = useState2(false);
1189
- const [currentZoom, setCurrentZoom] = useState2(initialZoom ?? DEFAULT_ZOOM);
1190
- const [isPopupOpen, setIsPopupOpen] = useState2(false);
1191
- const [isMobile, setIsMobile] = useState2(() => {
1192
- if (typeof window === "undefined") return false;
1193
- return window.matchMedia("(max-width: 768px)").matches;
1194
- });
1195
- const normalizedLayers = useMemo2(() => normalizeMapLayers(map), [map]);
1196
- useEffect5(() => {
1197
- if (typeof window === "undefined") return;
1198
- const mql = window.matchMedia("(max-width: 768px)");
1199
- const onChange = (e) => {
1200
- setIsMobile(e.matches);
1201
- };
1202
- setIsMobile(mql.matches);
1203
- if (typeof mql.addEventListener === "function") {
1204
- mql.addEventListener("change", onChange);
1205
- return () => mql.removeEventListener("change", onChange);
1206
- }
1207
- const legacy = mql;
1208
- if (typeof legacy.addListener === "function") {
1209
- legacy.addListener(onChange);
1210
- return () => legacy.removeListener(onChange);
1211
- }
1212
- return;
1213
- }, []);
1214
- useEffect5(() => {
1215
- if (featureInfoMode === "popup") {
1216
- ensurePopupStyles();
1217
- }
1218
- }, [featureInfoMode]);
1219
- useEffect5(() => {
1220
- if (featureInfoMode !== "popup") {
1221
- setIsPopupOpen(false);
1222
- }
1223
- }, [featureInfoMode]);
1224
- useEffect5(() => {
1225
- if (!mapInstance) return;
1226
- const popupPane = mapInstance.getPane("popupPane");
1227
- if (popupPane) {
1228
- popupPane.style.zIndex = "800";
1229
- }
1230
- const labelsPane = mapInstance.getPane(LABELS_PANE_NAME) ?? mapInstance.createPane(LABELS_PANE_NAME);
1231
- labelsPane.style.zIndex = "600";
1232
- }, [mapInstance]);
1233
- useEffect5(() => {
1234
- if (!mapInstance) return;
1235
- const handlePopupOpen = () => setIsPopupOpen(true);
1236
- const handlePopupClose = () => setIsPopupOpen(false);
1237
- mapInstance.on("popupopen", handlePopupOpen);
1238
- mapInstance.on("popupclose", handlePopupClose);
1239
- return () => {
1240
- mapInstance.off("popupopen", handlePopupOpen);
1241
- mapInstance.off("popupclose", handlePopupClose);
1242
- };
1243
- }, [mapInstance]);
1244
- const layerStyleIndex = useMemo2(() => {
1245
- const index = /* @__PURE__ */ new Map();
1246
- (map?.mapLayers ?? []).forEach((entry) => {
1247
- const layerStyle = entry.layer?.style ?? entry.mapLayer?.layer?.style ?? entry.style ?? null;
1248
- const layerId = entry.layerId ?? entry.mapLayer?.layerId ?? entry.mapLayer?.layer?.id ?? entry.layer?.id;
1249
- if (layerId !== void 0 && layerId !== null && layerStyle) {
1250
- index.set(String(layerId), layerStyle);
1251
- }
1252
- });
1253
- return index;
1254
- }, [map]);
1255
- const labelKeyIndex = useMemo2(() => {
1256
- const index = /* @__PURE__ */ new Map();
1257
- normalizedLayers.forEach((entry) => {
1258
- const label = entry.layer?.label ?? entry.mapLayer?.label ?? entry.mapLayer.layerConfig?.label;
1259
- if (typeof label === "string" && label.trim().length > 0) {
1260
- index.set(String(entry.layerId), label);
1261
- }
1262
- });
1263
- return index;
1264
- }, [normalizedLayers]);
1265
- const layerMetaIndex = useMemo2(() => {
1266
- const index = /* @__PURE__ */ new Map();
1267
- normalizedLayers.forEach((entry) => {
1268
- index.set(String(entry.layerId), {
1269
- layerType: typeof entry.layer?.layerType === "string" ? entry.layer.layerType : typeof entry.mapLayer.layerType === "string" ? entry.mapLayer.layerType : void 0,
1270
- geometryType: typeof entry.layer?.geometryType === "string" ? entry.layer.geometryType : typeof entry.mapLayer.layerConfig?.geometryType === "string" ? entry.mapLayer.layerConfig.geometryType : void 0
1271
- });
1272
- });
1273
- return index;
1274
- }, [normalizedLayers]);
1275
- const overlayStyleFunction = useMemo2(() => {
1276
- return (feature) => {
1277
- const featureLayerId = getFeatureLayerId(feature);
1278
- const featureStyleOverrides = getFeatureStyleOverrides(feature);
1279
- let style = null;
1280
- if (featureLayerId !== null) {
1281
- style = getStyleByLayerId(featureLayerId, mapLayers) ?? layerStyleIndex.get(String(featureLayerId)) ?? null;
1282
- }
1283
- const resolvedStyle = featureStyleOverrides ? { ...style ?? {}, ...featureStyleOverrides } : style;
1284
- const defaultOptions = {
1285
- color: resolvedStyle?.color ?? resolvedStyle?.fillColor ?? "#2563eb",
1286
- weight: resolvedStyle?.weight ?? 2,
1287
- fillColor: resolvedStyle?.fillColor ?? resolvedStyle?.color ?? "#2563eb",
1288
- fillOpacity: resolvedStyle?.fillOpacity ?? 0.15,
1289
- ...overlayStyle ?? {}
1290
- };
1291
- return defaultOptions;
1292
- };
1293
- }, [layerStyleIndex, mapLayers, overlayStyle]);
1294
- const overlayStyleFn = useCallback2(
1295
- (feature, _layerType, _baseOpacity) => overlayStyleFunction(feature),
1296
- [overlayStyleFunction]
1297
- );
1298
- const [baseStates, setBaseStates] = useState2([]);
1299
- const [mapOverrides, setMapOverrides] = useState2([]);
1300
- const [controlOverrides, setControlOverrides] = useState2([]);
1301
- const [uiOverrides, setUiOverrides] = useState2([]);
1302
- useEffect5(() => {
1303
- let isMounted = true;
1304
- setLoadingMap(true);
1305
- setMapError(null);
1306
- onLoadingChange?.("map", true);
1307
- client.maps.getMap(mapId, true).then((mapResponse) => {
1308
- if (!isMounted) return;
1309
- const extracted = extractMapDto(mapResponse);
1310
- const resolved = extracted ? normalizeMapCenter(extracted) : null;
1311
- setMap(resolved);
1312
- }).catch((err) => {
1313
- if (!isMounted) return;
1314
- setMapError(err instanceof Error ? err.message : "No se pudo obtener el mapa");
1315
- onError?.(err, "map");
1316
- }).finally(() => {
1317
- if (!isMounted) return;
1318
- setLoadingMap(false);
1319
- onLoadingChange?.("map", false);
1320
- });
1321
- return () => {
1322
- isMounted = false;
1323
- };
1324
- }, [client.maps, mapId, onError, onLoadingChange]);
1325
- useEffect5(() => {
1326
- if (normalizedLayers.length === 0) {
1327
- setLayers([]);
1328
- setBaseStates([]);
1329
- setMapOverrides([]);
1330
- setUiOverrides([]);
1331
- return;
1332
- }
1333
- const nextLayers = normalizedLayers.map((entry) => ({
1334
- mapLayer: { ...entry.mapLayer, layerId: entry.layerId },
1335
- layer: entry.layer,
1336
- displayOrder: entry.displayOrder
1337
- }));
1338
- const base = initLayerStates(
1339
- normalizedLayers.map((entry) => ({
1340
- ...entry.mapLayer,
1341
- layerId: entry.layerId,
1342
- opacity: entry.opacity,
1343
- isVisible: entry.isVisible
1344
- }))
1345
- );
1346
- const initialOverrides = normalizedLayers.map((entry) => ({
1347
- layerId: entry.layerId,
1348
- overrideVisible: entry.isVisible,
1349
- overrideOpacity: entry.opacity
1350
- }));
1351
- setLayers(nextLayers);
1352
- setBaseStates(base);
1353
- setMapOverrides(initialOverrides);
1354
- setUiOverrides([]);
1355
- }, [normalizedLayers]);
1356
- useEffect5(() => {
1357
- if (!layerControls) {
1358
- setControlOverrides([]);
1359
- return;
1360
- }
1361
- const overrides = layerControls.map((entry) => ({
1362
- layerId: entry.layerId,
1363
- overrideVisible: entry.visible,
1364
- overrideOpacity: entry.opacity
1365
- }));
1366
- setControlOverrides(overrides);
1367
- }, [layerControls]);
1368
- useEffect5(() => {
1369
- if (layerStates) {
1370
- return;
1371
- }
1372
- if (Array.isArray(layerControls) && layerControls.length === 0 && effectiveStates.length > 0) {
1373
- const reset = resetOverrides(baseStates);
1374
- setEffectiveStates(reset);
1375
- onLayerStateChange?.(reset);
1376
- }
1377
- }, [baseStates, effectiveStates.length, layerControls, layerStates, onLayerStateChange]);
1378
- useEffect5(() => {
1379
- if (layerStates) {
1380
- setEffectiveStates(layerStates);
1381
- }
1382
- }, [layerStates]);
1383
- useEffect5(() => {
1384
- if (layerStates) {
1385
- return;
1386
- }
1387
- if (mapOverrides.length === 0 && controlOverrides.length === 0 && uiOverrides.length === 0) {
1388
- setEffectiveStates(baseStates);
1389
- return;
1390
- }
1391
- const combined = [...mapOverrides, ...controlOverrides, ...uiOverrides];
1392
- const next = applyLayerOverrides(baseStates, combined);
1393
- setEffectiveStates(next);
1394
- onLayerStateChange?.(next);
1395
- }, [baseStates, controlOverrides, layerStates, mapOverrides, onLayerStateChange, uiOverrides]);
1396
- useEffect5(() => {
1397
- if (!Array.isArray(layerControls) || layerControls.length > 0) return;
1398
- setUiOverrides([]);
1399
- }, [layerControls]);
1400
- useEffect5(() => {
1401
- if (layerStates) {
1402
- return;
1403
- }
1404
- if (!Array.isArray(layerControls) && effectiveStates.length > 0 && baseStates.length > 0) {
1405
- const next = resetOverrides(baseStates);
1406
- setEffectiveStates(next);
1407
- onLayerStateChange?.(next);
1408
- }
1409
- }, [baseStates, effectiveStates.length, layerControls, layerStates, onLayerStateChange]);
1410
- const upsertUiOverride = (layerId, patch) => {
1411
- setUiOverrides((prev) => {
1412
- const filtered = prev.filter((entry) => entry.layerId !== layerId);
1413
- const nextEntry = {
1414
- layerId,
1415
- ...patch
1416
- };
1417
- return [...filtered, nextEntry];
1418
- });
1419
- };
1420
- const updateOpacityFromUi = useCallback2(
1421
- (layerId, uiOpacity) => {
1422
- const meta = layerMetaIndex.get(String(layerId));
1423
- const baseOpacity = clampOpacity3(uiOpacity);
1424
- const effectiveOpacity = calculateZoomBasedOpacity(
1425
- currentZoom,
1426
- baseOpacity,
1427
- meta?.layerType,
1428
- meta?.geometryType
1429
- );
1430
- if (layerStates && onLayerStateChange) {
1431
- const next = effectiveStates.map(
1432
- (state) => state.layerId === layerId ? {
1433
- ...state,
1434
- baseOpacity,
1435
- opacity: effectiveOpacity,
1436
- overrideOpacity: uiOpacity
1437
- } : state
1438
- );
1439
- onLayerStateChange(next);
1440
- return;
1441
- }
1442
- setBaseStates(
1443
- (prev) => prev.map((state) => state.layerId === layerId ? { ...state, baseOpacity } : state)
1444
- );
1445
- setUiOverrides((prev) => {
1446
- const existing = prev.find((entry) => entry.layerId === layerId);
1447
- const filtered = prev.filter((entry) => entry.layerId !== layerId);
1448
- if (existing && existing.overrideVisible !== void 0) {
1449
- return [
1450
- ...filtered,
1451
- { layerId, overrideVisible: existing.overrideVisible }
1452
- ];
1453
- }
1454
- return filtered;
1455
- });
1456
- },
1457
- [currentZoom, effectiveStates, layerMetaIndex, layerStates, onLayerStateChange]
1458
- );
1459
- const center = useMemo2(() => {
1460
- if (initialCenter) {
1461
- return initialCenter;
1462
- }
1463
- const normalized = normalizeCenterTuple(map?.settings?.center);
1464
- if (normalized) {
1465
- return normalized;
1466
- }
1467
- return DEFAULT_CENTER;
1468
- }, [initialCenter, map?.settings?.center]);
1469
- const zoom = initialZoom ?? map?.settings?.zoom ?? DEFAULT_ZOOM;
1470
- useEffect5(() => {
1471
- setCurrentZoom(zoom);
1472
- }, [zoom]);
1473
- useEffect5(() => {
1474
- if (!layerGeojson) return;
1475
- layers.forEach((layer) => {
1476
- const layerKey = String(layer.mapLayer.layerId);
1477
- const incoming = layerGeojson[layer.mapLayer.layerId] ?? layerGeojson[layerKey] ?? null;
1478
- if (incoming && !originalGeojsonByLayerIdRef.current[layerKey]) {
1479
- originalGeojsonByLayerIdRef.current[layerKey] = incoming;
1480
- }
1481
- });
1482
- }, [layerGeojson, layers]);
1483
- const decoratedLayers = useMemo2(() => {
1484
- return layers.map((layer) => {
1485
- const layerKey = String(layer.mapLayer.layerId);
1486
- const override = layerGeojsonOverrides[layerKey];
1487
- return {
1488
- ...layer,
1489
- effective: effectiveStates.find((state) => state.layerId === layer.mapLayer.layerId),
1490
- data: override ?? layerGeojson?.[layer.mapLayer.layerId] ?? layerGeojson?.[layerKey] ?? null
1491
- };
1492
- });
1493
- }, [effectiveStates, layerGeojson, layerGeojsonOverrides, layers]);
1494
- const orderedLayers = useMemo2(() => {
1495
- return [...decoratedLayers].filter((layer) => layer.effective?.visible && layer.data).sort((a, b) => a.displayOrder - b.displayOrder);
1496
- }, [decoratedLayers]);
1497
- const autoZoomGeojson = useMemo2(
1498
- () => orderedLayers.map((layer) => layer.data).filter((collection) => !!collection),
1499
- [orderedLayers]
1500
- );
1501
- const resolveLayerStyle = useCallback2(
1502
- (layerId) => {
1503
- return getStyleByLayerId(layerId, mapLayers) ?? layerStyleIndex.get(String(layerId)) ?? null;
1504
- },
1505
- [layerStyleIndex, mapLayers]
1506
- );
1507
- const labelMarkers = useMemo2(() => {
1508
- const markers = [];
1509
- decoratedLayers.forEach((layerState) => {
1510
- if (!layerState.effective?.visible) return;
1511
- const labelKey = labelKeyIndex.get(String(layerState.mapLayer.layerId));
1512
- if (!labelKey) return;
1513
- const data = layerState.data;
1514
- if (!data) return;
1515
- const resolvedStyle = resolveLayerStyle(layerState.mapLayer.layerId);
1516
- const layerColor = resolvedStyle?.fillColor ?? resolvedStyle?.color ?? "rgba(37, 99, 235, 1)";
1517
- const meta = layerMetaIndex.get(String(layerState.mapLayer.layerId));
1518
- const baseOpacity = layerState.effective?.baseOpacity ?? layerState.effective?.opacity ?? 1;
1519
- const opacity = calculateZoomBasedOpacity(
1520
- currentZoom,
1521
- baseOpacity,
1522
- meta?.layerType,
1523
- meta?.geometryType
1524
- );
1525
- data.features.forEach((feature, index) => {
1526
- const properties = feature.properties;
1527
- const value = properties?.[labelKey];
1528
- if (value === void 0 || value === null || value === "") return;
1529
- const bbox = computeBBoxFromFeature(feature);
1530
- if (!bbox) return;
1531
- const [lat, lng] = centroidFromBBox(bbox);
1532
- markers.push({
1533
- key: `${layerState.mapLayer.layerId}-label-${index}`,
1534
- position: [lat, lng],
1535
- label: String(value),
1536
- opacity,
1537
- layerId: layerState.mapLayer.layerId,
1538
- color: layerColor
1539
- });
1540
- });
1541
- });
1542
- return markers;
1543
- }, [currentZoom, decoratedLayers, labelKeyIndex, layerMetaIndex, resolveLayerStyle]);
1544
- const ensureLayerPanes = useCallback2(
1545
- (targetMap, targetLayers) => {
1546
- const baseZIndex = 400;
1547
- targetLayers.forEach((layer) => {
1548
- const order = Number.isFinite(layer.displayOrder) ? layer.displayOrder : 0;
1549
- const orderOffset = Math.max(0, Math.min(order, 150));
1550
- const fillPaneName = `zenit-layer-${layer.layerId}-fill`;
1551
- const pointPaneName = `zenit-layer-${layer.layerId}-points`;
1552
- const fillPane = targetMap.getPane(fillPaneName) ?? targetMap.createPane(fillPaneName);
1553
- const pointPane = targetMap.getPane(pointPaneName) ?? targetMap.createPane(pointPaneName);
1554
- fillPane.style.zIndex = String(baseZIndex + orderOffset);
1555
- pointPane.style.zIndex = String(baseZIndex + orderOffset + 100);
1556
- });
1557
- },
1558
- []
1559
- );
1560
- const handleMapReady = useCallback2(
1561
- (instance) => {
1562
- setPanesReady(false);
1563
- setMapInstance(instance);
1564
- onMapReady?.(instance);
1565
- },
1566
- [onMapReady]
1567
- );
1568
- useEffect5(() => {
1569
- if (!mapInstance) {
1570
- setPanesReady(false);
1571
- return;
1572
- }
1573
- if (orderedLayers.length === 0) {
1574
- return;
1575
- }
1576
- const layerTargets = orderedLayers.map((layer) => ({
1577
- layerId: layer.mapLayer.layerId,
1578
- displayOrder: layer.displayOrder
1579
- }));
1580
- ensureLayerPanes(mapInstance, layerTargets);
1581
- const first = layerTargets[0];
1582
- const testPane = mapInstance.getPane(`zenit-layer-${first.layerId}-fill`);
1583
- const labelsPane = mapInstance.getPane(LABELS_PANE_NAME);
1584
- if (testPane && labelsPane) {
1585
- setPanesReady(true);
1586
- }
1587
- }, [mapInstance, orderedLayers, ensureLayerPanes]);
1588
- const overlayOnEachFeature = useMemo2(() => {
1589
- return (feature, layer) => {
1590
- const layerId = getFeatureLayerId(feature) ?? void 0;
1591
- const geometryType = feature?.geometry?.type;
1592
- const isPointFeature = geometryType === "Point" || geometryType === "MultiPoint" || layer instanceof L4.CircleMarker;
1593
- const originalStyle = layer instanceof L4.Path ? {
1594
- color: layer.options.color,
1595
- weight: layer.options.weight,
1596
- fillColor: layer.options.fillColor,
1597
- opacity: layer.options.opacity,
1598
- fillOpacity: layer.options.fillOpacity
1599
- } : null;
1600
- const originalRadius = layer instanceof L4.CircleMarker ? layer.getRadius() : null;
1601
- if (featureInfoMode === "popup") {
1602
- const content = buildFeaturePopupHtml(feature);
1603
- if (content) {
1604
- const { maxWidth, minWidth, maxHeight } = getPopupDimensions();
1605
- layer.bindPopup(content, {
1606
- maxWidth,
1607
- minWidth,
1608
- maxHeight,
1609
- className: "custom-leaflet-popup",
1610
- autoPan: true,
1611
- closeButton: true,
1612
- keepInView: true
1613
- });
1614
- }
1615
- }
1616
- if (isPointFeature && layer.bindTooltip) {
1617
- layer.bindTooltip("Click para ver detalle", {
1618
- sticky: true,
1619
- direction: "top",
1620
- opacity: 0.9,
1621
- className: "zenit-map-tooltip"
1622
- });
1623
- }
1624
- layer.on("click", () => {
1625
- if (featureInfoMode === "popup" && client && layerId !== void 0 && !extractDescriptionValue(feature?.properties) && feature?.geometry) {
1626
- const trackedFeature = feature;
1627
- if (!trackedFeature.__zenit_popup_loaded) {
1628
- trackedFeature.__zenit_popup_loaded = true;
1629
- client.layers.getLayerGeoJsonIntersect({
1630
- id: layerId,
1631
- geometry: feature.geometry
1632
- }).then((response) => {
1633
- const geo = extractGeoJsonFeatureCollection(response);
1634
- const candidates = geo?.features ?? [];
1635
- const resolved = pickIntersectFeature(feature, candidates);
1636
- if (!resolved?.properties) return;
1637
- const mergedProperties = {
1638
- ...trackedFeature.properties ?? {},
1639
- ...resolved.properties
1640
- };
1641
- trackedFeature.properties = mergedProperties;
1642
- const updatedHtml = buildFeaturePopupHtml({
1643
- ...feature,
1644
- properties: mergedProperties
1645
- });
1646
- if (updatedHtml && layer.setPopupContent) {
1647
- layer.setPopupContent(updatedHtml);
1648
- }
1649
- }).catch(() => {
1650
- trackedFeature.__zenit_popup_loaded = false;
1651
- });
1652
- }
1653
- }
1654
- onFeatureClick?.(feature, layerId);
1655
- });
1656
- layer.on("mouseover", () => {
1657
- if (layer instanceof L4.Path && originalStyle) {
1658
- layer.setStyle({
1659
- ...originalStyle,
1660
- weight: (originalStyle.weight ?? 2) + 1,
1661
- opacity: Math.min(1, (originalStyle.opacity ?? 1) + 0.2),
1662
- fillOpacity: Math.min(1, (originalStyle.fillOpacity ?? 0.8) + 0.1)
1663
- });
1664
- }
1665
- if (layer instanceof L4.CircleMarker && typeof originalRadius === "number") {
1666
- layer.setRadius(originalRadius + 1);
1667
- }
1668
- onFeatureHover?.(feature, layerId);
1669
- });
1670
- layer.on("mouseout", () => {
1671
- if (layer instanceof L4.Path && originalStyle) {
1672
- layer.setStyle(originalStyle);
1673
- }
1674
- if (layer instanceof L4.CircleMarker && typeof originalRadius === "number") {
1675
- layer.setRadius(originalRadius);
1676
- }
1677
- });
1678
- };
1679
- }, [client, featureInfoMode, onFeatureClick, onFeatureHover]);
1680
- const buildLayerStyle = (layerId, baseOpacity, feature, layerType) => {
1681
- const style = resolveLayerStyle(layerId);
1682
- const featureStyleOverrides = getFeatureStyleOverrides(feature);
1683
- const resolvedStyle = featureStyleOverrides ? { ...style ?? {}, ...featureStyleOverrides } : style;
1684
- const geometryType = feature?.geometry?.type;
1685
- const resolvedLayerType = layerType ?? geometryType;
1686
- return layerStyleToLeaflet({
1687
- baseOpacity,
1688
- zoom: currentZoom,
1689
- layerStyle: resolvedStyle,
1690
- geometryType,
1691
- layerType: resolvedLayerType
1692
- });
1693
- };
1694
- const makeStyleFnForLayer = (layerId) => {
1695
- return (feature, layerType, baseOpacity) => {
1696
- return buildLayerStyle(layerId, baseOpacity ?? 1, feature, layerType);
1697
- };
1698
- };
1699
- useImperativeHandle(ref, () => ({
1700
- setLayerOpacity: (layerId, opacity) => {
1701
- upsertUiOverride(layerId, { overrideOpacity: opacity });
1702
- },
1703
- setLayerVisibility: (layerId, visible) => {
1704
- upsertUiOverride(layerId, { overrideVisible: visible });
1705
- },
1706
- fitBounds: (bbox, options) => {
1707
- if (!mapInstance) return;
1708
- if (typeof bbox.minLat !== "number" || typeof bbox.minLon !== "number" || typeof bbox.maxLat !== "number" || typeof bbox.maxLon !== "number" || !Number.isFinite(bbox.minLat) || !Number.isFinite(bbox.minLon) || !Number.isFinite(bbox.maxLat) || !Number.isFinite(bbox.maxLon)) {
1709
- console.warn("[ZenitMap.fitBounds] Invalid bbox: missing or non-finite coordinates", bbox);
1710
- return;
1711
- }
1712
- const bounds = [
1713
- [bbox.minLat, bbox.minLon],
1714
- [bbox.maxLat, bbox.maxLon]
1715
- ];
1716
- const fitOptions = {
1717
- padding: options?.padding ?? [12, 12],
1718
- animate: options?.animate ?? true
1719
- };
1720
- mapInstance.fitBounds(bounds, fitOptions);
1721
- },
1722
- fitToBbox: (bbox, padding) => {
1723
- if (!mapInstance) return;
1724
- if (typeof bbox.minLat !== "number" || typeof bbox.minLon !== "number" || typeof bbox.maxLat !== "number" || typeof bbox.maxLon !== "number" || !Number.isFinite(bbox.minLat) || !Number.isFinite(bbox.minLon) || !Number.isFinite(bbox.maxLat) || !Number.isFinite(bbox.maxLon)) {
1725
- console.warn("[ZenitMap.fitToBbox] Invalid bbox", bbox);
1726
- return;
1727
- }
1728
- const resolvedPadding = padding ?? (isMobile ? 40 : 24);
1729
- const bounds = L4.latLngBounds([bbox.minLat, bbox.minLon], [bbox.maxLat, bbox.maxLon]);
1730
- mapInstance.fitBounds(bounds, { padding: [resolvedPadding, resolvedPadding], animate: true });
1731
- },
1732
- fitToGeoJson: (fc, padding) => {
1733
- if (!mapInstance) return;
1734
- if (!fc?.features?.length) return;
1735
- const bounds = L4.geoJSON(fc).getBounds();
1736
- if (!bounds.isValid()) return;
1737
- const resolvedPadding = padding ?? (isMobile ? 40 : 24);
1738
- mapInstance.fitBounds(bounds, { padding: [resolvedPadding, resolvedPadding], animate: true });
1739
- },
1740
- setView: (coordinates, zoom2) => {
1741
- if (!mapInstance) return;
1742
- mapInstance.setView([coordinates.lat, coordinates.lon], zoom2 ?? mapInstance.getZoom(), {
1743
- animate: true
1744
- });
1745
- },
1746
- getLayerSnapshot: () => {
1747
- return effectiveStates.map((state) => ({
1748
- layerId: state.layerId,
1749
- visible: state.visible,
1750
- opacity: state.opacity
1751
- }));
1752
- },
1753
- restoreLayerSnapshot: (snapshot) => {
1754
- const overrides = snapshot.map((s) => ({
1755
- layerId: s.layerId,
1756
- overrideVisible: s.visible,
1757
- overrideOpacity: s.opacity
1758
- }));
1759
- setUiOverrides(overrides);
1760
- },
1761
- highlightFeature: (layerId, featureId) => {
1762
- upsertUiOverride(layerId, { overrideVisible: true, overrideOpacity: 1 });
1763
- },
1764
- updateLayerGeoJson: (layerId, featureCollection) => {
1765
- const layerKey = String(layerId);
1766
- setLayerGeojsonOverrides((prev) => ({ ...prev, [layerKey]: featureCollection }));
1767
- },
1768
- restoreLayerGeoJson: (layerId) => {
1769
- const layerKey = String(layerId);
1770
- const original = originalGeojsonByLayerIdRef.current[layerKey];
1771
- setLayerGeojsonOverrides((prev) => {
1772
- const next = { ...prev };
1773
- if (original) {
1774
- next[layerKey] = original;
1775
- } else {
1776
- delete next[layerKey];
1777
- }
1778
- return next;
1779
- });
1780
- },
1781
- getMapInstance: () => mapInstance
1782
- }), [effectiveStates, isMobile, mapInstance]);
1783
- if (loadingMap) {
1784
- return /* @__PURE__ */ jsx3("div", { style: { padding: 16, height, width }, children: "Cargando mapa..." });
1785
- }
1786
- if (mapError) {
1787
- return /* @__PURE__ */ jsxs3("div", { style: { padding: 16, height, width, color: "red" }, children: [
1788
- "Error al cargar mapa: ",
1789
- mapError
1790
- ] });
1791
- }
1792
- if (!map) {
1793
- return null;
1794
- }
1795
- const handleZoomChange = (zoomValue) => {
1796
- setCurrentZoom(zoomValue);
1797
- onZoomChange?.(zoomValue);
1798
- };
1799
- return /* @__PURE__ */ jsxs3(
1800
- "div",
1801
- {
1802
- style: {
1803
- display: "flex",
1804
- height,
1805
- width,
1806
- border: "1px solid #e2e8f0",
1807
- borderRadius: 4,
1808
- overflow: "hidden",
1809
- boxSizing: "border-box"
1810
- },
1811
- children: [
1812
- /* @__PURE__ */ jsx3(
1813
- "div",
1814
- {
1815
- className: `zenit-map-shell${isPopupOpen ? " popup-open" : ""}`,
1816
- style: { flex: 1, position: "relative" },
1817
- children: /* @__PURE__ */ jsxs3(
1818
- MapContainer,
1819
- {
1820
- center,
1821
- zoom,
1822
- style: { height: "100%", width: "100%" },
1823
- scrollWheelZoom: true,
1824
- zoomControl: false,
1825
- children: [
1826
- /* @__PURE__ */ jsx3(
1827
- TileLayer,
1828
- {
1829
- url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
1830
- attribution: "\xA9 OpenStreetMap contributors"
1831
- }
1832
- ),
1833
- /* @__PURE__ */ jsx3(ZoomControl, { position: "topright" }),
1834
- /* @__PURE__ */ jsx3(MapInstanceBridge, { onReady: handleMapReady }),
1835
- /* @__PURE__ */ jsx3(MapInvalidator, { trigger: mapId }),
1836
- /* @__PURE__ */ jsx3(
1837
- BBoxZoomHandler,
1838
- {
1839
- bbox: zoomToBbox ?? void 0,
1840
- geojson: zoomToGeojson ?? void 0,
1841
- autoGeojson: autoZoomGeojson
1842
- }
1843
- ),
1844
- /* @__PURE__ */ jsx3(ZoomBasedOpacityHandler, { onZoomChange: handleZoomChange }),
1845
- /* @__PURE__ */ jsx3(LocationControl, {}),
1846
- orderedLayers.map((layerState) => {
1847
- const baseOpacity = layerState.effective?.baseOpacity ?? layerState.effective?.opacity ?? 1;
1848
- const fillPaneName = `zenit-layer-${layerState.mapLayer.layerId}-fill`;
1849
- const pointsPaneName = `zenit-layer-${layerState.mapLayer.layerId}-points`;
1850
- const layerType = layerState.layer?.layerType ?? layerState.mapLayer.layerType ?? void 0;
1851
- const labelKey = labelKeyIndex.get(String(layerState.mapLayer.layerId));
1852
- return /* @__PURE__ */ jsxs3(React4.Fragment, { children: [
1853
- layerState.data && /* @__PURE__ */ jsx3(
1854
- LayerGeoJson,
1855
- {
1856
- layerId: layerState.mapLayer.layerId,
1857
- data: layerState.data,
1858
- baseOpacity,
1859
- isMobile,
1860
- panesReady,
1861
- mapInstance,
1862
- fillPaneName,
1863
- pointsPaneName,
1864
- layerType,
1865
- styleFn: makeStyleFnForLayer(layerState.mapLayer.layerId),
1866
- onEachFeature: overlayOnEachFeature,
1867
- onPolygonLabel: labelKey ? (feature, layer) => {
1868
- const geometryType = feature?.geometry?.type;
1869
- if (geometryType !== "Polygon" && geometryType !== "MultiPolygon") return;
1870
- const properties = feature?.properties;
1871
- const value = properties?.[labelKey];
1872
- if (!value || !layer.bindTooltip) return;
1873
- layer.bindTooltip(String(value), {
1874
- sticky: true,
1875
- direction: "center",
1876
- opacity: 0.9,
1877
- className: "polygon-label-tooltip"
1878
- });
1879
- } : void 0
1880
- }
1881
- ),
1882
- panesReady && mapInstance?.getPane(LABELS_PANE_NAME) ? labelMarkers.filter(
1883
- (marker) => String(marker.layerId) === String(layerState.mapLayer.layerId)
1884
- ).map((marker) => /* @__PURE__ */ jsx3(
1885
- Marker2,
1886
- {
1887
- position: marker.position,
1888
- icon: createCustomIcon(marker.label, marker.opacity, marker.color),
1889
- interactive: false,
1890
- pane: LABELS_PANE_NAME
1891
- },
1892
- marker.key
1893
- )) : null
1894
- ] }, layerState.mapLayer.layerId.toString());
1895
- }),
1896
- overlayGeojson && /* @__PURE__ */ jsx3(
1897
- LayerGeoJson,
1898
- {
1899
- layerId: "overlay-geojson",
1900
- data: overlayGeojson,
1901
- baseOpacity: 1,
1902
- isMobile,
1903
- panesReady,
1904
- mapInstance,
1905
- fillPaneName: "zenit-overlay-fill",
1906
- pointsPaneName: "zenit-overlay-points",
1907
- styleFn: overlayStyleFn,
1908
- onEachFeature: overlayOnEachFeature
1909
- }
1910
- )
1911
- ]
1912
- },
1913
- String(mapId)
1914
- )
1915
- }
1916
- ),
1917
- showLayerPanel && decoratedLayers.length > 0 && /* @__PURE__ */ jsxs3(
1918
- "div",
1919
- {
1920
- style: {
1921
- width: 260,
1922
- borderLeft: "1px solid #e2e8f0",
1923
- padding: "12px 16px",
1924
- background: "#fafafa",
1925
- fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1926
- fontSize: 14,
1927
- overflowY: "auto"
1928
- },
1929
- children: [
1930
- overlayGeojson && /* @__PURE__ */ jsxs3(
1931
- "div",
1932
- {
1933
- style: {
1934
- border: "1px solid #bfdbfe",
1935
- background: "#eff6ff",
1936
- color: "#1e40af",
1937
- padding: "8px 10px",
1938
- borderRadius: 8,
1939
- marginBottom: 12
1940
- },
1941
- children: [
1942
- /* @__PURE__ */ jsx3("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "Overlay activo" }),
1943
- /* @__PURE__ */ jsxs3("div", { style: { fontSize: 13 }, children: [
1944
- "GeoJSON externo con ",
1945
- (overlayGeojson.features?.length ?? 0).toLocaleString(),
1946
- " elementos."
1947
- ] })
1948
- ]
1949
- }
1950
- ),
1951
- /* @__PURE__ */ jsx3("div", { style: { fontWeight: 600, marginBottom: 12 }, children: "Capas" }),
1952
- decoratedLayers.map((layerState) => /* @__PURE__ */ jsxs3(
1953
- "div",
1954
- {
1955
- style: { borderBottom: "1px solid #e5e7eb", paddingBottom: 10, marginBottom: 10 },
1956
- children: [
1957
- /* @__PURE__ */ jsxs3("label", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
1958
- /* @__PURE__ */ jsx3(
1959
- "input",
1960
- {
1961
- type: "checkbox",
1962
- checked: layerState.effective?.visible ?? false,
1963
- onChange: (e) => {
1964
- const visible = e.target.checked;
1965
- upsertUiOverride(layerState.mapLayer.layerId, { overrideVisible: visible });
1966
- }
1967
- }
1968
- ),
1969
- /* @__PURE__ */ jsx3("span", { children: layerState.layer?.name ?? `Capa ${layerState.mapLayer.layerId}` })
1970
- ] }),
1971
- /* @__PURE__ */ jsxs3("div", { style: { marginTop: 8 }, children: [
1972
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 4 }, children: [
1973
- /* @__PURE__ */ jsx3("span", { style: { color: "#4a5568" }, children: "Opacidad" }),
1974
- /* @__PURE__ */ jsxs3("span", { children: [
1975
- Math.round((layerState.effective?.opacity ?? 1) * 100),
1976
- "%"
1977
- ] })
1978
- ] }),
1979
- /* @__PURE__ */ jsx3(
1980
- "input",
1981
- {
1982
- type: "range",
1983
- min: 0,
1984
- max: 1,
1985
- step: 0.05,
1986
- value: layerState.effective?.opacity ?? 1,
1987
- onChange: (e) => {
1988
- const value = Number(e.target.value);
1989
- updateOpacityFromUi(layerState.mapLayer.layerId, value);
1990
- },
1991
- style: { width: "100%" }
1992
- }
1993
- )
1994
- ] })
1995
- ]
1996
- },
1997
- layerState.mapLayer.layerId.toString()
1998
- ))
1999
- ]
2000
- }
2001
- )
2002
- ]
2003
- }
2004
- );
2005
- });
2006
- ZenitMap.displayName = "ZenitMap";
2007
-
2008
- // src/react/ZenitLayerManager.tsx
2009
- import React5, { useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState3 } from "react";
2010
-
2011
- // src/react/icons.tsx
2012
- import { Eye, EyeOff, ChevronLeft, ChevronRight, Layers, Upload, X, ZoomIn } from "lucide-react";
2013
-
2014
- // src/react/ZenitLayerManager.tsx
2015
- import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2016
- var FLOAT_TOLERANCE = 1e-3;
2017
- function areEffectiveStatesEqual(a, b) {
2018
- if (a.length !== b.length) return false;
2019
- return a.every((state, index) => {
2020
- const other = b[index];
2021
- if (!other) return false;
2022
- const opacityDiff = Math.abs((state.opacity ?? 1) - (other.opacity ?? 1));
2023
- return String(state.layerId) === String(other.layerId) && (state.visible ?? false) === (other.visible ?? false) && opacityDiff <= FLOAT_TOLERANCE;
2024
- });
2025
- }
2026
- function getLayerColor2(style) {
2027
- if (!style) return "#94a3b8";
2028
- return style.fillColor ?? style.color ?? "#94a3b8";
2029
- }
2030
- function resolveDisplayOrder(value) {
2031
- if (typeof value === "number" && Number.isFinite(value)) return value;
2032
- if (typeof value === "string") {
2033
- const parsed = Number.parseFloat(value);
2034
- if (Number.isFinite(parsed)) return parsed;
2035
- }
2036
- return 0;
2037
- }
2038
- var ZenitLayerManager = ({
2039
- client,
2040
- mapId,
2041
- side = "right",
2042
- className,
2043
- style,
2044
- height,
2045
- layerStates,
2046
- onLayerStatesChange,
2047
- mapZoom,
2048
- autoOpacityOnZoom = false,
2049
- autoOpacityConfig,
2050
- showUploadTab = true,
2051
- showLayerVisibilityIcon = true,
2052
- layerFeatureCounts,
2053
- mapLayers,
2054
- onApplyLayerFilter,
2055
- onClearLayerFilter
2056
- }) => {
2057
- const [map, setMap] = useState3(null);
2058
- const [loadingMap, setLoadingMap] = useState3(false);
2059
- const [mapError, setMapError] = useState3(null);
2060
- const [layers, setLayers] = useState3([]);
2061
- const [activeTab, setActiveTab] = useState3("layers");
2062
- const [panelVisible, setPanelVisible] = useState3(true);
2063
- const [selectedFilterLayerId, setSelectedFilterLayerId] = useState3("");
2064
- const [selectedFilterField, setSelectedFilterField] = useState3("");
2065
- const [selectedFilterValue, setSelectedFilterValue] = useState3("");
2066
- const [catalogByLayerField, setCatalogByLayerField] = useState3({});
2067
- const [loadingCatalog, setLoadingCatalog] = useState3(false);
2068
- const [applyingFilter, setApplyingFilter] = useState3(false);
2069
- const [filterError, setFilterError] = useState3(null);
2070
- const [appliedFilter, setAppliedFilter] = useState3(null);
2071
- const catalogAbortRef = useRef6(null);
2072
- const lastEmittedStatesRef = useRef6(null);
2073
- const isControlled = Array.isArray(layerStates) && typeof onLayerStatesChange === "function";
2074
- const baseStates = useMemo3(
2075
- () => initLayerStates(
2076
- layers.map((entry) => ({
2077
- ...entry.mapLayer,
2078
- layerId: entry.mapLayer.layerId,
2079
- isVisible: entry.visible,
2080
- opacity: entry.opacity
2081
- }))
2082
- ),
2083
- [layers]
2084
- );
2085
- const overrideStates = useMemo3(
2086
- () => layers.map(
2087
- (entry) => ({
2088
- layerId: entry.mapLayer.layerId,
2089
- overrideVisible: entry.visible,
2090
- overrideOpacity: entry.opacity
2091
- })
2092
- ),
2093
- [layers]
2094
- );
2095
- const effectiveStates = useMemo3(
2096
- () => layerStates ?? applyLayerOverrides(baseStates, overrideStates),
2097
- [baseStates, layerStates, overrideStates]
2098
- );
2099
- const layerMetaIndex = useMemo3(() => {
2100
- const index = /* @__PURE__ */ new Map();
2101
- mapLayers?.forEach((entry) => {
2102
- const key = String(entry.layerId);
2103
- index.set(key, { layerType: entry.layerType ?? void 0, geometryType: entry.geometryType ?? void 0 });
2104
- });
2105
- map?.mapLayers?.forEach((entry) => {
2106
- const key = String(entry.layerId);
2107
- if (!index.has(key)) {
2108
- index.set(key, { layerType: entry.layerType ?? void 0, geometryType: entry.geometryType ?? void 0 });
2109
- }
2110
- });
2111
- return index;
2112
- }, [map, mapLayers]);
2113
- const resolveUserOpacity = React5.useCallback((state) => {
2114
- if (typeof state.overrideOpacity === "number") return state.overrideOpacity;
2115
- if (typeof state.overrideOpacity === "string") {
2116
- const parsed = Number.parseFloat(state.overrideOpacity);
2117
- if (Number.isFinite(parsed)) return parsed;
2118
- }
2119
- return state.opacity ?? 1;
2120
- }, []);
2121
- const resolveEffectiveOpacity = React5.useCallback(
2122
- (layerId, userOpacity) => {
2123
- if (!autoOpacityOnZoom || typeof mapZoom !== "number") {
2124
- return userOpacity;
2125
- }
2126
- const meta = layerMetaIndex.get(String(layerId));
2127
- return getEffectiveLayerOpacity(
2128
- userOpacity,
2129
- mapZoom,
2130
- meta?.layerType,
2131
- meta?.geometryType,
2132
- autoOpacityConfig
2133
- );
2134
- },
2135
- [autoOpacityConfig, autoOpacityOnZoom, layerMetaIndex, mapZoom]
2136
- );
2137
- const effectiveStatesWithZoom = useMemo3(() => {
2138
- if (!autoOpacityOnZoom || typeof mapZoom !== "number") {
2139
- return effectiveStates;
2140
- }
2141
- return effectiveStates.map((state) => {
2142
- const userOpacity = resolveUserOpacity(state);
2143
- const adjustedOpacity = resolveEffectiveOpacity(state.layerId, userOpacity);
2144
- return {
2145
- ...state,
2146
- opacity: adjustedOpacity,
2147
- overrideOpacity: userOpacity
2148
- };
2149
- });
2150
- }, [autoOpacityOnZoom, effectiveStates, mapZoom, resolveEffectiveOpacity, resolveUserOpacity]);
2151
- useEffect6(() => {
2152
- let cancelled = false;
2153
- setLoadingMap(true);
2154
- setMapError(null);
2155
- setLayers([]);
2156
- client.maps.getMap(mapId, true).then((mapResponse) => {
2157
- if (cancelled) return;
2158
- const extractedMap = extractMapDto(mapResponse);
2159
- const resolvedMap = extractedMap ? normalizeMapCenter(extractedMap) : null;
2160
- setMap(resolvedMap);
2161
- const normalizedLayers = normalizeMapLayers(resolvedMap);
2162
- setLayers(
2163
- normalizedLayers.map((entry, index) => ({
2164
- mapLayer: { ...entry.mapLayer, layerId: entry.layerId },
2165
- layer: entry.layer,
2166
- visible: entry.isVisible,
2167
- opacity: entry.opacity,
2168
- displayOrder: entry.displayOrder,
2169
- initialIndex: index
2170
- }))
2171
- );
2172
- }).catch((err) => {
2173
- if (cancelled) return;
2174
- const message = err instanceof Error ? err.message : "No se pudo cargar el mapa";
2175
- setMapError(message);
2176
- }).finally(() => {
2177
- if (!cancelled) setLoadingMap(false);
2178
- });
2179
- return () => {
2180
- cancelled = true;
2181
- };
2182
- }, [client.maps, mapId]);
2183
- useEffect6(() => {
2184
- if (!showUploadTab && activeTab === "upload") {
2185
- setActiveTab("layers");
2186
- }
2187
- }, [activeTab, showUploadTab]);
2188
- useEffect6(() => {
2189
- if (isControlled) return;
2190
- if (!onLayerStatesChange) return;
2191
- const emitStates = autoOpacityOnZoom && typeof mapZoom === "number" ? effectiveStatesWithZoom : effectiveStates;
2192
- const previous = lastEmittedStatesRef.current;
2193
- if (previous && areEffectiveStatesEqual(previous, emitStates)) {
2194
- return;
2195
- }
2196
- lastEmittedStatesRef.current = emitStates;
2197
- onLayerStatesChange(emitStates);
2198
- }, [
2199
- autoOpacityOnZoom,
2200
- effectiveStates,
2201
- effectiveStatesWithZoom,
2202
- isControlled,
2203
- mapZoom,
2204
- onLayerStatesChange
2205
- ]);
2206
- const updateLayerVisible = React5.useCallback(
2207
- (layerId, visible) => {
2208
- if (!onLayerStatesChange) return;
2209
- const next = effectiveStates.map(
2210
- (state) => state.layerId === layerId ? { ...state, visible, overrideVisible: visible } : state
2211
- );
2212
- onLayerStatesChange(next);
2213
- },
2214
- [effectiveStates, onLayerStatesChange]
2215
- );
2216
- const updateLayerOpacity = React5.useCallback(
2217
- (layerId, opacity) => {
2218
- if (!onLayerStatesChange) return;
2219
- const adjustedOpacity = resolveEffectiveOpacity(layerId, opacity);
2220
- const next = effectiveStates.map(
2221
- (state) => state.layerId === layerId ? { ...state, opacity: adjustedOpacity, overrideOpacity: opacity } : state
2222
- );
2223
- onLayerStatesChange(next);
2224
- },
2225
- [effectiveStates, onLayerStatesChange, resolveEffectiveOpacity]
2226
- );
2227
- const resolveFeatureCount = React5.useCallback(
2228
- (layerId, layer) => {
2229
- const resolvedFeatureCount = layerFeatureCounts?.[layerId] ?? layerFeatureCounts?.[String(layerId)];
2230
- if (typeof resolvedFeatureCount === "number") return resolvedFeatureCount;
2231
- const featureCount = layer?.featuresCount ?? layer?.featureCount ?? layer?.totalFeatures;
2232
- return typeof featureCount === "number" ? featureCount : null;
2233
- },
2234
- [layerFeatureCounts]
2235
- );
2236
- const decoratedLayers = useMemo3(() => {
2237
- return layers.map((entry) => ({
2238
- ...entry,
2239
- effective: effectiveStates.find((state) => state.layerId === entry.mapLayer.layerId),
2240
- featureCount: resolveFeatureCount(entry.mapLayer.layerId, entry.layer),
2241
- layerName: entry.layer?.name ?? entry.mapLayer.name ?? `Capa ${entry.mapLayer.layerId}`
2242
- })).sort((a, b) => {
2243
- const aOrder = resolveDisplayOrder(
2244
- a.displayOrder ?? a.mapLayer.displayOrder ?? a.mapLayer.order
2245
- );
2246
- const bOrder = resolveDisplayOrder(
2247
- b.displayOrder ?? b.mapLayer.displayOrder ?? b.mapLayer.order
2248
- );
2249
- const aHasOrder = a.displayOrder ?? a.mapLayer.displayOrder ?? a.mapLayer.order ?? null;
2250
- const bHasOrder = b.displayOrder ?? b.mapLayer.displayOrder ?? b.mapLayer.order ?? null;
2251
- if (aHasOrder !== null && bHasOrder !== null) {
2252
- const orderCompare = aOrder - bOrder;
2253
- if (orderCompare !== 0) return orderCompare;
2254
- }
2255
- const aInitial = a.initialIndex ?? 0;
2256
- const bInitial = b.initialIndex ?? 0;
2257
- if (aInitial !== bInitial) return aInitial - bInitial;
2258
- const aName = a.layerName ?? String(a.mapLayer.layerId);
2259
- const bName = b.layerName ?? String(b.mapLayer.layerId);
2260
- const nameCompare = aName.localeCompare(bName, void 0, { sensitivity: "base" });
2261
- if (nameCompare !== 0) return nameCompare;
2262
- return String(a.mapLayer.layerId).localeCompare(String(b.mapLayer.layerId));
2263
- });
2264
- }, [effectiveStates, layers, resolveFeatureCount]);
2265
- const filterableLayers = useMemo3(() => {
2266
- return decoratedLayers.filter((entry) => {
2267
- const prefilters = entry.mapLayer.layerConfig?.prefilters;
2268
- return !!prefilters && Object.keys(prefilters).length > 0;
2269
- });
2270
- }, [decoratedLayers]);
2271
- const selectedFilterLayer = useMemo3(
2272
- () => filterableLayers.find((layer) => String(layer.mapLayer.layerId) === selectedFilterLayerId) ?? null,
2273
- [filterableLayers, selectedFilterLayerId]
2274
- );
2275
- const filterFields = useMemo3(() => {
2276
- const prefilters = selectedFilterLayer?.mapLayer.layerConfig?.prefilters;
2277
- return prefilters ? Object.keys(prefilters) : [];
2278
- }, [selectedFilterLayer]);
2279
- const activeCatalogKey = selectedFilterLayer ? `${selectedFilterLayer.mapLayer.layerId}:${selectedFilterField}` : null;
2280
- const activeCatalogValues = activeCatalogKey ? catalogByLayerField[activeCatalogKey] ?? [] : [];
2281
- const extractCatalogValues = React5.useCallback((catalogData, field) => {
2282
- const values = /* @__PURE__ */ new Set();
2283
- const pushValue = (value) => {
2284
- if (value === null || value === void 0) return;
2285
- const normalized = String(value).trim();
2286
- if (normalized) values.add(normalized);
2287
- };
2288
- if (catalogData && typeof catalogData === "object") {
2289
- const maybeRecord = catalogData;
2290
- const directField = maybeRecord[field];
2291
- if (Array.isArray(directField)) {
2292
- directField.forEach(pushValue);
2293
- }
2294
- const items = maybeRecord.items;
2295
- if (Array.isArray(items)) {
2296
- items.forEach((item) => {
2297
- if (!item || typeof item !== "object") return;
2298
- const row = item;
2299
- const rowField = row.field;
2300
- if (String(rowField ?? "").toUpperCase() === field.toUpperCase() && Array.isArray(row.values)) {
2301
- row.values.forEach(pushValue);
2302
- }
2303
- });
2304
- }
2305
- const features = maybeRecord.features;
2306
- if (Array.isArray(features)) {
2307
- features.forEach((feature) => {
2308
- if (!feature || typeof feature !== "object") return;
2309
- const properties = feature.properties;
2310
- if (properties && field in properties) {
2311
- pushValue(properties[field]);
2312
- }
2313
- });
2314
- }
2315
- }
2316
- return [...values].sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
2317
- }, []);
2318
- useEffect6(() => {
2319
- if (!filterableLayers.length) {
2320
- setSelectedFilterLayerId("");
2321
- return;
2322
- }
2323
- if (!selectedFilterLayerId || !filterableLayers.some((layer) => String(layer.mapLayer.layerId) === selectedFilterLayerId)) {
2324
- setSelectedFilterLayerId(String(filterableLayers[0].mapLayer.layerId));
2325
- }
2326
- }, [filterableLayers, selectedFilterLayerId]);
2327
- useEffect6(() => {
2328
- if (!filterFields.length) {
2329
- setSelectedFilterField("");
2330
- return;
2331
- }
2332
- if (!selectedFilterField || !filterFields.includes(selectedFilterField)) {
2333
- setSelectedFilterField(filterFields[0]);
2334
- setSelectedFilterValue("");
2335
- }
2336
- }, [filterFields, selectedFilterField]);
2337
- useEffect6(() => {
2338
- if (activeTab !== "filters") return;
2339
- if (!selectedFilterLayer || !selectedFilterField || !activeCatalogKey) return;
2340
- if (catalogByLayerField[activeCatalogKey]) return;
2341
- catalogAbortRef.current?.abort();
2342
- const controller = new AbortController();
2343
- catalogAbortRef.current = controller;
2344
- setLoadingCatalog(true);
2345
- setFilterError(null);
2346
- client.layers.getLayerFeaturesCatalog(selectedFilterLayer.mapLayer.layerId).then((response) => {
2347
- if (controller.signal.aborted) return;
2348
- const values = extractCatalogValues(response.data, selectedFilterField);
2349
- setCatalogByLayerField((prev) => ({ ...prev, [activeCatalogKey]: values }));
2350
- }).catch((error) => {
2351
- if (controller.signal.aborted) return;
2352
- const message = error instanceof Error ? error.message : "No se pudo cargar el cat\xE1logo";
2353
- setFilterError(message);
2354
- }).finally(() => {
2355
- if (!controller.signal.aborted) setLoadingCatalog(false);
2356
- });
2357
- return () => {
2358
- controller.abort();
2359
- };
2360
- }, [activeCatalogKey, activeTab, catalogByLayerField, client.layers, extractCatalogValues, selectedFilterField, selectedFilterLayer]);
2361
- const handleApplyFilter = React5.useCallback(async () => {
2362
- if (!selectedFilterLayer || !selectedFilterField || !selectedFilterValue || !onApplyLayerFilter) return;
2363
- setApplyingFilter(true);
2364
- setFilterError(null);
2365
- try {
2366
- await onApplyLayerFilter({
2367
- layerId: selectedFilterLayer.mapLayer.layerId,
2368
- field: selectedFilterField,
2369
- value: selectedFilterValue
2370
- });
2371
- setAppliedFilter({
2372
- layerId: selectedFilterLayer.mapLayer.layerId,
2373
- field: selectedFilterField,
2374
- value: selectedFilterValue
2375
- });
2376
- } catch (error) {
2377
- const message = error instanceof Error ? error.message : "No se pudo aplicar el filtro";
2378
- setFilterError(message);
2379
- } finally {
2380
- setApplyingFilter(false);
2381
- }
2382
- }, [onApplyLayerFilter, selectedFilterField, selectedFilterLayer, selectedFilterValue]);
2383
- const handleClearFilter = React5.useCallback(async () => {
2384
- if (!selectedFilterLayer) return;
2385
- setApplyingFilter(true);
2386
- setFilterError(null);
2387
- try {
2388
- await onClearLayerFilter?.({ layerId: selectedFilterLayer.mapLayer.layerId, field: selectedFilterField || void 0 });
2389
- setSelectedFilterValue("");
2390
- setAppliedFilter(null);
2391
- } catch (error) {
2392
- const message = error instanceof Error ? error.message : "No se pudo limpiar el filtro";
2393
- setFilterError(message);
2394
- } finally {
2395
- setApplyingFilter(false);
2396
- }
2397
- }, [onClearLayerFilter, selectedFilterField, selectedFilterLayer]);
2398
- const resolveLayerStyle = React5.useCallback(
2399
- (layerId) => {
2400
- const layerKey = String(layerId);
2401
- const fromProp = mapLayers?.find((entry) => String(entry.layerId) === layerKey)?.style;
2402
- if (fromProp) return fromProp;
2403
- const fromMapDto = map?.mapLayers?.find((entry) => String(entry.layerId) === layerKey);
2404
- if (fromMapDto?.layer) {
2405
- const layer = fromMapDto.layer;
2406
- if (layer.style) return layer.style;
2407
- }
2408
- return void 0;
2409
- },
2410
- [map, mapLayers]
2411
- );
2412
- const panelStyle = {
2413
- width: 360,
2414
- borderLeft: side === "right" ? "1px solid #e2e8f0" : void 0,
2415
- borderRight: side === "left" ? "1px solid #e2e8f0" : void 0,
2416
- background: "#f1f5f9",
2417
- fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
2418
- fontSize: 14,
2419
- boxSizing: "border-box",
2420
- height: height ?? "100%",
2421
- display: "flex",
2422
- flexDirection: "column",
2423
- boxShadow: "0 12px 28px rgba(15, 23, 42, 0.08)",
2424
- ...style,
2425
- ...height ? { height } : {}
2426
- };
2427
- if (loadingMap) {
2428
- return /* @__PURE__ */ jsx4("div", { className, style: panelStyle, children: "Cargando capas\u2026" });
2429
- }
2430
- if (mapError) {
2431
- return /* @__PURE__ */ jsxs4("div", { className, style: { ...panelStyle, color: "#c53030" }, children: [
2432
- "Error al cargar mapa: ",
2433
- mapError
2434
- ] });
2435
- }
2436
- if (!map) {
2437
- return null;
2438
- }
2439
- const headerStyle = {
2440
- padding: "14px 16px",
2441
- borderBottom: "1px solid #e2e8f0",
2442
- background: "#fff",
2443
- position: "sticky",
2444
- top: 0,
2445
- zIndex: 2,
2446
- boxShadow: "0 1px 0 rgba(148, 163, 184, 0.25)"
2447
- };
2448
- const renderLayerCards = () => {
2449
- return /* @__PURE__ */ jsx4("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: decoratedLayers.map((layerState) => {
2450
- const layerId = layerState.mapLayer.layerId;
2451
- const layerName = layerState.layerName ?? `Capa ${layerId}`;
2452
- const visible = layerState.effective?.visible ?? false;
2453
- const userOpacity = layerState.effective ? resolveUserOpacity(layerState.effective) : 1;
2454
- const featureCount = layerState.featureCount;
2455
- const layerColor = getLayerColor2(resolveLayerStyle(layerId));
2456
- const muted = !visible;
2457
- const opacityPercent = Math.round(userOpacity * 100);
2458
- const sliderBackground = `linear-gradient(to right, ${layerColor} 0%, ${layerColor} ${opacityPercent}%, #e5e7eb ${opacityPercent}%, #e5e7eb 100%)`;
2459
- return /* @__PURE__ */ jsxs4(
2460
- "div",
2461
- {
2462
- className: `zlm-card${muted ? " is-muted" : ""}`,
2463
- style: {
2464
- border: "1px solid #e2e8f0",
2465
- borderRadius: 12,
2466
- padding: 12,
2467
- background: "#fff",
2468
- display: "flex",
2469
- flexDirection: "column",
2470
- gap: 10,
2471
- width: "100%"
2472
- },
2473
- children: [
2474
- /* @__PURE__ */ jsxs4("div", { style: { display: "flex", justifyContent: "space-between", gap: 12 }, children: [
2475
- /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 10, alignItems: "flex-start", minWidth: 0, flex: 1 }, children: [
2476
- /* @__PURE__ */ jsx4(
2477
- "div",
2478
- {
2479
- style: {
2480
- width: 14,
2481
- height: 14,
2482
- borderRadius: "50%",
2483
- backgroundColor: layerColor,
2484
- border: "2px solid #e2e8f0",
2485
- flexShrink: 0,
2486
- marginTop: 4
2487
- },
2488
- title: "Color de la capa"
2489
- }
2490
- ),
2491
- showLayerVisibilityIcon && /* @__PURE__ */ jsx4(
2492
- "button",
2493
- {
2494
- type: "button",
2495
- className: `zlm-icon-button${visible ? " is-active" : ""}`,
2496
- onClick: () => isControlled ? updateLayerVisible(layerId, !visible) : setLayers(
2497
- (prev) => prev.map(
2498
- (entry) => entry.mapLayer.layerId === layerId ? { ...entry, visible: !visible } : entry
2499
- )
2500
- ),
2501
- "aria-label": visible ? "Ocultar capa" : "Mostrar capa",
2502
- children: visible ? /* @__PURE__ */ jsx4(Eye, { size: 16 }) : /* @__PURE__ */ jsx4(EyeOff, { size: 16 })
2503
- }
2504
- ),
2505
- /* @__PURE__ */ jsxs4("div", { style: { minWidth: 0, flex: 1 }, children: [
2506
- /* @__PURE__ */ jsx4(
2507
- "div",
2508
- {
2509
- className: "zlm-layer-name",
2510
- style: {
2511
- fontWeight: 700,
2512
- display: "-webkit-box",
2513
- WebkitLineClamp: 2,
2514
- WebkitBoxOrient: "vertical",
2515
- overflow: "hidden",
2516
- overflowWrap: "anywhere",
2517
- lineHeight: 1.2,
2518
- color: muted ? "#64748b" : "#0f172a"
2519
- },
2520
- title: layerName,
2521
- children: layerName
2522
- }
2523
- ),
2524
- /* @__PURE__ */ jsxs4("div", { style: { color: muted ? "#94a3b8" : "#64748b", fontSize: 12 }, children: [
2525
- "ID ",
2526
- layerId
2527
- ] })
2528
- ] })
2529
- ] }),
2530
- /* @__PURE__ */ jsx4("div", { style: { display: "flex", alignItems: "flex-start", gap: 6, flexShrink: 0 }, children: typeof featureCount === "number" && /* @__PURE__ */ jsxs4("span", { className: "zlm-badge", children: [
2531
- featureCount.toLocaleString(),
2532
- " features"
2533
- ] }) })
2534
- ] }),
2535
- /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 10, alignItems: "center" }, children: /* @__PURE__ */ jsxs4("div", { style: { flex: 1 }, children: [
2536
- /* @__PURE__ */ jsxs4("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 6, color: "#64748b", fontSize: 12 }, children: [
2537
- /* @__PURE__ */ jsx4("span", { children: "Opacidad" }),
2538
- /* @__PURE__ */ jsxs4("span", { children: [
2539
- opacityPercent,
2540
- "%"
2541
- ] })
2542
- ] }),
2543
- /* @__PURE__ */ jsx4(
2544
- "input",
2545
- {
2546
- className: "zlm-range",
2547
- type: "range",
2548
- min: 0,
2549
- max: 1,
2550
- step: 0.05,
2551
- value: userOpacity,
2552
- style: {
2553
- width: "100%",
2554
- accentColor: layerColor,
2555
- background: sliderBackground,
2556
- height: 6,
2557
- borderRadius: 999
2558
- },
2559
- onChange: (e) => {
2560
- const value = Number(e.target.value);
2561
- if (isControlled) {
2562
- updateLayerOpacity(layerId, value);
2563
- return;
2564
- }
2565
- setLayers(
2566
- (prev) => prev.map(
2567
- (entry) => entry.mapLayer.layerId === layerId ? { ...entry, opacity: value } : entry
2568
- )
2569
- );
2570
- },
2571
- "aria-label": `Opacidad de la capa ${layerName}`
2572
- }
2573
- )
2574
- ] }) })
2575
- ]
2576
- },
2577
- layerId.toString()
2578
- );
2579
- }) });
2580
- };
2581
- return /* @__PURE__ */ jsxs4("div", { className: ["zenit-layer-manager", className].filter(Boolean).join(" "), style: panelStyle, children: [
2582
- /* @__PURE__ */ jsx4("style", { children: `
2583
- .zenit-layer-manager .zlm-card {
2584
- transition: box-shadow 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
2585
- box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
2586
- }
2587
- .zenit-layer-manager .zlm-card.is-muted {
2588
- opacity: 0.7;
2589
- }
2590
- .zenit-layer-manager .zlm-badge {
2591
- display: inline-flex;
2592
- align-items: center;
2593
- gap: 4px;
2594
- margin-top: 6px;
2595
- padding: 2px 8px;
2596
- border-radius: 999px;
2597
- background: #f1f5f9;
2598
- color: #475569;
2599
- font-size: 11px;
2600
- font-weight: 600;
2601
- }
2602
- .zenit-layer-manager .zlm-icon-button {
2603
- border: 1px solid #e2e8f0;
2604
- background: #f8fafc;
2605
- color: #475569;
2606
- border-radius: 8px;
2607
- width: 34px;
2608
- height: 34px;
2609
- display: inline-flex;
2610
- align-items: center;
2611
- justify-content: center;
2612
- cursor: pointer;
2613
- transition: all 0.15s ease;
2614
- }
2615
- .zenit-layer-manager .zlm-icon-button.is-active {
2616
- background: #0f172a;
2617
- color: #fff;
2618
- border-color: #0f172a;
2619
- }
2620
- .zenit-layer-manager .zlm-icon-button:hover {
2621
- box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
2622
- }
2623
- .zenit-layer-manager .zlm-icon-button:focus-visible {
2624
- outline: 2px solid #60a5fa;
2625
- outline-offset: 2px;
2626
- }
2627
- .zenit-layer-manager .zlm-range {
2628
- width: 100%;
2629
- accent-color: #0f172a;
2630
- }
2631
- .zenit-layer-manager .zlm-tab {
2632
- flex: 1;
2633
- padding: 8px 12px;
2634
- border: none;
2635
- background: transparent;
2636
- color: #475569;
2637
- font-weight: 600;
2638
- cursor: pointer;
2639
- display: inline-flex;
2640
- align-items: center;
2641
- justify-content: center;
2642
- gap: 6px;
2643
- font-size: 13px;
2644
- }
2645
- .zenit-layer-manager .zlm-tab.is-active {
2646
- background: #fff;
2647
- color: #0f172a;
2648
- box-shadow: 0 4px 10px rgba(15, 23, 42, 0.08);
2649
- border-radius: 10px;
2650
- }
2651
- .zenit-layer-manager .zlm-tab:focus-visible {
2652
- outline: 2px solid #60a5fa;
2653
- outline-offset: 2px;
2654
- }
2655
- .zenit-layer-manager .zlm-panel-toggle {
2656
- border: 1px solid #e2e8f0;
2657
- background: #fff;
2658
- color: #0f172a;
2659
- border-radius: 10px;
2660
- padding: 6px 10px;
2661
- display: inline-flex;
2662
- align-items: center;
2663
- gap: 6px;
2664
- font-size: 12px;
2665
- font-weight: 600;
2666
- cursor: pointer;
2667
- transition: all 0.15s ease;
2668
- }
2669
- .zenit-layer-manager .zlm-panel-toggle:hover {
2670
- box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
2671
- }
2672
- .zenit-layer-manager .zlm-panel-toggle:focus-visible {
2673
- outline: 2px solid #60a5fa;
2674
- outline-offset: 2px;
2675
- }
2676
- ` }),
2677
- /* @__PURE__ */ jsxs4("div", { style: headerStyle, children: [
2678
- /* @__PURE__ */ jsxs4("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
2679
- /* @__PURE__ */ jsxs4("div", { children: [
2680
- /* @__PURE__ */ jsx4("div", { style: { fontWeight: 800, fontSize: 16, color: "#0f172a" }, children: "Gesti\xF3n de Capas" }),
2681
- /* @__PURE__ */ jsxs4("div", { style: { color: "#64748b", fontSize: 12 }, children: [
2682
- "Mapa #",
2683
- map.id
2684
- ] })
2685
- ] }),
2686
- /* @__PURE__ */ jsxs4(
2687
- "button",
2688
- {
2689
- type: "button",
2690
- onClick: () => setPanelVisible((prev) => !prev),
2691
- className: "zlm-panel-toggle",
2692
- "aria-label": panelVisible ? "Ocultar panel de capas" : "Mostrar panel de capas",
2693
- children: [
2694
- panelVisible ? /* @__PURE__ */ jsx4(Eye, { size: 16 }) : /* @__PURE__ */ jsx4(EyeOff, { size: 16 }),
2695
- panelVisible ? "Ocultar" : "Mostrar"
2696
- ]
2697
- }
2698
- )
2699
- ] }),
2700
- /* @__PURE__ */ jsxs4(
2701
- "div",
2702
- {
2703
- style: {
2704
- display: "flex",
2705
- gap: 6,
2706
- marginTop: 12,
2707
- padding: 4,
2708
- border: "1px solid #e2e8f0",
2709
- borderRadius: 12,
2710
- background: "#f1f5f9"
2711
- },
2712
- children: [
2713
- /* @__PURE__ */ jsxs4(
2714
- "button",
2715
- {
2716
- type: "button",
2717
- className: `zlm-tab${activeTab === "layers" ? " is-active" : ""}`,
2718
- onClick: () => setActiveTab("layers"),
2719
- children: [
2720
- /* @__PURE__ */ jsx4(Layers, { size: 16 }),
2721
- "Capas"
2722
- ]
2723
- }
2724
- ),
2725
- showUploadTab && /* @__PURE__ */ jsxs4(
2726
- "button",
2727
- {
2728
- type: "button",
2729
- className: `zlm-tab${activeTab === "upload" ? " is-active" : ""}`,
2730
- onClick: () => setActiveTab("upload"),
2731
- children: [
2732
- /* @__PURE__ */ jsx4(Upload, { size: 16 }),
2733
- "Subir"
2734
- ]
2735
- }
2736
- ),
2737
- /* @__PURE__ */ jsxs4(
2738
- "button",
2739
- {
2740
- type: "button",
2741
- className: `zlm-tab${activeTab === "filters" ? " is-active" : ""}`,
2742
- onClick: () => setActiveTab("filters"),
2743
- children: [
2744
- /* @__PURE__ */ jsx4(Layers, { size: 16 }),
2745
- "Filtros"
2746
- ]
2747
- }
2748
- )
2749
- ]
2750
- }
2751
- )
2752
- ] }),
2753
- panelVisible && /* @__PURE__ */ jsxs4("div", { style: { padding: "12px 10px 18px", overflowY: "auto", flex: 1, minHeight: 0 }, children: [
2754
- activeTab === "layers" && renderLayerCards(),
2755
- activeTab === "filters" && /* @__PURE__ */ jsx4("div", { className: "zlm-filter-panel", style: { display: "flex", flexDirection: "column", gap: 10, background: "#fff", border: "1px solid #e2e8f0", borderRadius: 12, padding: 12 }, children: !filterableLayers.length ? /* @__PURE__ */ jsx4("div", { style: { color: "#64748b", fontSize: 13 }, children: "No hay filtros disponibles para las capas de este mapa." }) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
2756
- filterableLayers.length > 1 && /* @__PURE__ */ jsxs4("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, color: "#475569" }, children: [
2757
- "Capa",
2758
- /* @__PURE__ */ jsx4("select", { className: "zlm-filter-select", value: selectedFilterLayerId, onChange: (e) => setSelectedFilterLayerId(e.target.value), children: filterableLayers.map((layer) => /* @__PURE__ */ jsx4("option", { value: String(layer.mapLayer.layerId), children: layer.layerName ?? `Capa ${layer.mapLayer.layerId}` }, String(layer.mapLayer.layerId))) })
2759
- ] }),
2760
- /* @__PURE__ */ jsxs4("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, color: "#475569" }, children: [
2761
- "Campo",
2762
- /* @__PURE__ */ jsx4("select", { className: "zlm-filter-select", value: selectedFilterField, onChange: (e) => {
2763
- setSelectedFilterField(e.target.value);
2764
- setSelectedFilterValue("");
2765
- }, children: filterFields.map((field) => /* @__PURE__ */ jsx4("option", { value: field, children: field }, field)) })
2766
- ] }),
2767
- /* @__PURE__ */ jsxs4("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, color: "#475569" }, children: [
2768
- "Valor",
2769
- /* @__PURE__ */ jsxs4(
2770
- "select",
2771
- {
2772
- className: "zlm-filter-select",
2773
- value: selectedFilterValue,
2774
- onChange: (e) => {
2775
- const value = e.target.value;
2776
- setSelectedFilterValue(value);
2777
- },
2778
- children: [
2779
- /* @__PURE__ */ jsx4("option", { value: "", children: "Seleccionar\u2026" }),
2780
- activeCatalogValues.map((value) => /* @__PURE__ */ jsx4("option", { value, children: value }, value))
2781
- ]
2782
- }
2783
- )
2784
- ] }),
2785
- loadingCatalog && /* @__PURE__ */ jsx4("div", { style: { color: "#64748b", fontSize: 12 }, children: "Cargando cat\xE1logo\u2026" }),
2786
- !loadingCatalog && selectedFilterField && activeCatalogValues.length === 0 && /* @__PURE__ */ jsx4("div", { style: { color: "#64748b", fontSize: 12 }, children: "No hay cat\xE1logo disponible para este filtro." }),
2787
- filterError && /* @__PURE__ */ jsx4("div", { style: { color: "#b91c1c", fontSize: 12 }, children: filterError }),
2788
- appliedFilter && /* @__PURE__ */ jsxs4("div", { className: "zlm-badge", style: { alignSelf: "flex-start" }, children: [
2789
- "Activo: ",
2790
- appliedFilter.field,
2791
- " = ",
2792
- appliedFilter.value
2793
- ] }),
2794
- /* @__PURE__ */ jsxs4("div", { className: "zlm-filter-actions", style: { display: "flex", gap: 8 }, children: [
2795
- /* @__PURE__ */ jsx4(
2796
- "button",
2797
- {
2798
- type: "button",
2799
- className: "zlm-panel-toggle",
2800
- disabled: !selectedFilterValue || applyingFilter || !onApplyLayerFilter,
2801
- onClick: handleApplyFilter,
2802
- children: applyingFilter ? "Aplicando\u2026" : "Aplicar"
2803
- }
2804
- ),
2805
- /* @__PURE__ */ jsx4(
2806
- "button",
2807
- {
2808
- type: "button",
2809
- className: "zlm-panel-toggle",
2810
- disabled: applyingFilter,
2811
- onClick: handleClearFilter,
2812
- children: "Limpiar"
2813
- }
2814
- )
2815
- ] })
2816
- ] }) }),
2817
- showUploadTab && activeTab === "upload" && /* @__PURE__ */ jsx4("div", { style: { color: "#475569", fontSize: 13 }, children: "Pr\xF3ximamente podr\xE1s subir capas desde este panel." })
2818
- ] })
2819
- ] });
2820
- };
2821
-
2822
- // src/react/ZenitFeatureFilterPanel.tsx
2823
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2824
- var ZenitFeatureFilterPanel = ({
2825
- title = "Filtros",
2826
- description,
2827
- className,
2828
- style,
2829
- children
2830
- }) => {
2831
- return /* @__PURE__ */ jsxs5(
2832
- "section",
2833
- {
2834
- className,
2835
- style: {
2836
- border: "1px solid #e2e8f0",
2837
- borderRadius: 12,
2838
- padding: 16,
2839
- background: "#fff",
2840
- fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
2841
- ...style
2842
- },
2843
- children: [
2844
- /* @__PURE__ */ jsxs5("header", { style: { marginBottom: 12 }, children: [
2845
- /* @__PURE__ */ jsx5("h3", { style: { margin: 0, fontSize: 16 }, children: title }),
2846
- description && /* @__PURE__ */ jsx5("p", { style: { margin: "6px 0 0", color: "#475569", fontSize: 13 }, children: description })
2847
- ] }),
2848
- /* @__PURE__ */ jsx5("div", { children })
2849
- ]
2850
- }
2851
- );
2852
- };
2853
-
2854
- // src/react/ai/FloatingChatBox.tsx
2855
- import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef8, useState as useState5 } from "react";
2856
- import { createPortal as createPortal2 } from "react-dom";
2857
-
2858
- // src/react/hooks/use-chat.ts
2859
- import { useCallback as useCallback3, useRef as useRef7, useState as useState4 } from "react";
2860
-
2861
- // src/ai/chat.service.ts
2862
- var DEFAULT_ERROR_MESSAGE = "No fue posible completar la solicitud al asistente.";
2863
- var normalizeBaseUrl = (baseUrl) => baseUrl.replace(/\/$/, "");
2864
- var resolveBaseUrl = (config, options) => {
2865
- const baseUrl = options?.baseUrl ?? config?.baseUrl;
2866
- if (!baseUrl) {
2867
- throw new Error("baseUrl es requerido para usar el chat de Zenit AI.");
2868
- }
2869
- return normalizeBaseUrl(baseUrl);
2870
- };
2871
- var resolveAccessToken = async (config, options) => {
2872
- if (options?.accessToken) return options.accessToken;
2873
- if (config?.accessToken) return config.accessToken;
2874
- if (options?.getAccessToken) return await options.getAccessToken();
2875
- if (config?.getAccessToken) return await config.getAccessToken();
2876
- return void 0;
2877
- };
2878
- var buildAuthHeaders = (token) => {
2879
- if (!token) return {};
2880
- return { Authorization: `Bearer ${token}` };
2881
- };
2882
- var parseError = async (response) => {
2883
- try {
2884
- const contentType = response.headers.get("content-type") ?? "";
2885
- if (contentType.includes("application/json")) {
2886
- const payload = await response.json();
2887
- const message = payload?.message ?? DEFAULT_ERROR_MESSAGE;
2888
- return new Error(message);
2889
- }
2890
- const text = await response.text();
2891
- return new Error(text || DEFAULT_ERROR_MESSAGE);
2892
- } catch (error) {
2893
- return error instanceof Error ? error : new Error(DEFAULT_ERROR_MESSAGE);
2894
- }
2895
- };
2896
- var sendMessage = async (mapId, request, options, config) => {
2897
- const baseUrl = resolveBaseUrl(config, options);
2898
- const token = await resolveAccessToken(config, options);
2899
- const response = await fetch(`${baseUrl}/ai/maps/chat/${mapId}`, {
2900
- method: "POST",
2901
- headers: {
2902
- "Content-Type": "application/json",
2903
- ...buildAuthHeaders(token)
2904
- },
2905
- body: JSON.stringify(request),
2906
- signal: options?.signal
2907
- });
2908
- if (!response.ok) {
2909
- throw await parseError(response);
2910
- }
2911
- return await response.json();
2912
- };
2913
- var mergeResponse = (previous, chunk, aggregatedAnswer) => ({
2914
- ...previous ?? chunk,
2915
- ...chunk,
2916
- answer: aggregatedAnswer
2917
- });
2918
- var resolveAggregatedAnswer = (current, incoming) => {
2919
- if (!incoming) return current;
2920
- if (incoming.startsWith(current)) return incoming;
2921
- return current + incoming;
2922
- };
2923
- var sendMessageStream = async (mapId, request, callbacks = {}, options, config) => {
2924
- const baseUrl = resolveBaseUrl(config, options);
2925
- const token = await resolveAccessToken(config, options);
2926
- const response = await fetch(`${baseUrl}/ai/maps/chat/${mapId}/stream`, {
2927
- method: "POST",
2928
- headers: {
2929
- Accept: "text/event-stream",
2930
- "Content-Type": "application/json",
2931
- ...buildAuthHeaders(token)
2932
- },
2933
- body: JSON.stringify(request),
2934
- signal: options?.signal
2935
- });
2936
- if (!response.ok) {
2937
- const error = await parseError(response);
2938
- callbacks.onError?.(error);
2939
- throw error;
2940
- }
2941
- const reader = response.body?.getReader();
2942
- if (!reader) {
2943
- const error = new Error("No se pudo iniciar el streaming de Zenit AI.");
2944
- callbacks.onError?.(error);
2945
- throw error;
2946
- }
2947
- const decoder = new TextDecoder("utf-8");
2948
- let buffer = "";
2949
- let aggregated = "";
2950
- let latestResponse = null;
2951
- try {
2952
- while (true) {
2953
- const { value, done } = await reader.read();
2954
- if (done) break;
2955
- buffer += decoder.decode(value, { stream: true });
2956
- const lines = buffer.split(/\r?\n/);
2957
- buffer = lines.pop() ?? "";
2958
- for (const line of lines) {
2959
- const trimmed = line.trim();
2960
- if (!trimmed || trimmed.startsWith("event:")) continue;
2961
- if (!trimmed.startsWith("data:")) continue;
2962
- const payload = trimmed.replace(/^data:\s*/, "");
2963
- if (!payload || payload === "[DONE]") continue;
2964
- try {
2965
- const parsed = JSON.parse(payload);
2966
- aggregated = resolveAggregatedAnswer(aggregated, parsed.answer ?? "");
2967
- latestResponse = mergeResponse(latestResponse, parsed, aggregated);
2968
- if (latestResponse) {
2969
- callbacks.onChunk?.(latestResponse, aggregated);
2970
- }
2971
- } catch (error) {
2972
- callbacks.onError?.(error);
2973
- }
2974
- }
2975
- }
2976
- } catch (error) {
2977
- callbacks.onError?.(error);
2978
- throw error;
2979
- }
2980
- if (!latestResponse) {
2981
- const error = new Error(DEFAULT_ERROR_MESSAGE);
2982
- callbacks.onError?.(error);
2983
- throw error;
2984
- }
2985
- callbacks.onComplete?.(latestResponse);
2986
- return latestResponse;
2987
- };
2988
- var createChatService = (config) => ({
2989
- sendMessage: (mapId, request, options) => sendMessage(mapId, request, options, config),
2990
- sendMessageStream: (mapId, request, callbacks, options) => sendMessageStream(mapId, request, callbacks, options, config)
2991
- });
2992
-
2993
- // src/react/hooks/use-chat.ts
2994
- var useSendMessage = (config) => {
2995
- const [isLoading, setIsLoading] = useState4(false);
2996
- const [error, setError] = useState4(null);
2997
- const send = useCallback3(
2998
- async (mapId, request, options) => {
2999
- setIsLoading(true);
3000
- setError(null);
3001
- try {
3002
- return await sendMessage(mapId, request, options, config);
3003
- } catch (err) {
3004
- setError(err);
3005
- throw err;
3006
- } finally {
3007
- setIsLoading(false);
3008
- }
3009
- },
3010
- [config]
3011
- );
3012
- return { sendMessage: send, isLoading, error };
3013
- };
3014
- var useSendMessageStream = (config) => {
3015
- const [isStreaming, setIsStreaming] = useState4(false);
3016
- const [streamingText, setStreamingText] = useState4("");
3017
- const [completeResponse, setCompleteResponse] = useState4(null);
3018
- const [error, setError] = useState4(null);
3019
- const requestIdRef = useRef7(0);
3020
- const reset = useCallback3(() => {
3021
- setIsStreaming(false);
3022
- setStreamingText("");
3023
- setCompleteResponse(null);
3024
- setError(null);
3025
- }, []);
3026
- const send = useCallback3(
3027
- async (mapId, request, options) => {
3028
- const requestId = requestIdRef.current + 1;
3029
- requestIdRef.current = requestId;
3030
- setIsStreaming(true);
3031
- setStreamingText("");
3032
- setCompleteResponse(null);
3033
- setError(null);
3034
- try {
3035
- const response = await sendMessageStream(
3036
- mapId,
3037
- request,
3038
- {
3039
- onChunk: (_chunk, aggregated) => {
3040
- if (requestIdRef.current !== requestId) return;
3041
- setStreamingText(aggregated);
3042
- },
3043
- onError: (err) => {
3044
- if (requestIdRef.current !== requestId) return;
3045
- setError(err);
3046
- },
3047
- onComplete: (finalResponse) => {
3048
- if (requestIdRef.current !== requestId) return;
3049
- setCompleteResponse(finalResponse);
3050
- }
3051
- },
3052
- options,
3053
- config
3054
- );
3055
- return response;
3056
- } catch (err) {
3057
- setError(err);
3058
- throw err;
3059
- } finally {
3060
- if (requestIdRef.current === requestId) {
3061
- setIsStreaming(false);
3062
- }
3063
- }
3064
- },
3065
- [config]
3066
- );
3067
- return {
3068
- sendMessage: send,
3069
- isStreaming,
3070
- streamingText,
3071
- completeResponse,
3072
- error,
3073
- reset
3074
- };
3075
- };
3076
-
3077
- // src/react/components/MarkdownRenderer.tsx
3078
- import ReactMarkdown from "react-markdown";
3079
- import remarkGfm from "remark-gfm";
3080
- import { jsx as jsx6 } from "react/jsx-runtime";
3081
- function normalizeAssistantMarkdown(text) {
3082
- if (!text || typeof text !== "string") return "";
3083
- let normalized = text;
3084
- normalized = normalized.replace(/\r\n/g, "\n");
3085
- normalized = normalized.replace(/^\s*#{1,6}\s*$/gm, "");
3086
- normalized = normalized.replace(/\n{3,}/g, "\n\n");
3087
- normalized = normalized.split("\n").map((line) => line.trimEnd()).join("\n");
3088
- normalized = normalized.trim();
3089
- return normalized;
3090
- }
3091
- var MarkdownRenderer = ({ content, className }) => {
3092
- const normalizedContent = normalizeAssistantMarkdown(content);
3093
- if (!normalizedContent) {
3094
- return null;
3095
- }
3096
- return /* @__PURE__ */ jsx6("div", { className, style: { wordBreak: "break-word" }, children: /* @__PURE__ */ jsx6(
3097
- ReactMarkdown,
3098
- {
3099
- remarkPlugins: [remarkGfm],
3100
- components: {
3101
- // Headings with proper spacing
3102
- h1: ({ children, ...props }) => /* @__PURE__ */ jsx6("h1", { style: { fontSize: "1.5em", fontWeight: 700, marginTop: "1em", marginBottom: "0.5em" }, ...props, children }),
3103
- h2: ({ children, ...props }) => /* @__PURE__ */ jsx6("h2", { style: { fontSize: "1.3em", fontWeight: 700, marginTop: "0.9em", marginBottom: "0.45em" }, ...props, children }),
3104
- h3: ({ children, ...props }) => /* @__PURE__ */ jsx6("h3", { style: { fontSize: "1.15em", fontWeight: 600, marginTop: "0.75em", marginBottom: "0.4em" }, ...props, children }),
3105
- h4: ({ children, ...props }) => /* @__PURE__ */ jsx6("h4", { style: { fontSize: "1.05em", fontWeight: 600, marginTop: "0.6em", marginBottom: "0.35em" }, ...props, children }),
3106
- h5: ({ children, ...props }) => /* @__PURE__ */ jsx6("h5", { style: { fontSize: "1em", fontWeight: 600, marginTop: "0.5em", marginBottom: "0.3em" }, ...props, children }),
3107
- h6: ({ children, ...props }) => /* @__PURE__ */ jsx6("h6", { style: { fontSize: "0.95em", fontWeight: 600, marginTop: "0.5em", marginBottom: "0.3em" }, ...props, children }),
3108
- // Paragraphs with comfortable line height
3109
- p: ({ children, ...props }) => /* @__PURE__ */ jsx6("p", { style: { marginTop: "0.5em", marginBottom: "0.5em", lineHeight: 1.6 }, ...props, children }),
3110
- // Lists with proper indentation
3111
- ul: ({ children, ...props }) => /* @__PURE__ */ jsx6("ul", { style: { paddingLeft: "1.5em", marginTop: "0.5em", marginBottom: "0.5em" }, ...props, children }),
3112
- ol: ({ children, ...props }) => /* @__PURE__ */ jsx6("ol", { style: { paddingLeft: "1.5em", marginTop: "0.5em", marginBottom: "0.5em" }, ...props, children }),
3113
- li: ({ children, ...props }) => /* @__PURE__ */ jsx6("li", { style: { marginTop: "0.25em", marginBottom: "0.25em" }, ...props, children }),
3114
- // Code blocks
3115
- code: ({ inline, children, ...props }) => {
3116
- if (inline) {
3117
- return /* @__PURE__ */ jsx6(
3118
- "code",
3119
- {
3120
- style: {
3121
- backgroundColor: "rgba(0, 0, 0, 0.08)",
3122
- padding: "0.15em 0.4em",
3123
- borderRadius: "4px",
3124
- fontSize: "0.9em",
3125
- fontFamily: "monospace"
3126
- },
3127
- ...props,
3128
- children
3129
- }
3130
- );
3131
- }
3132
- return /* @__PURE__ */ jsx6(
3133
- "code",
3134
- {
3135
- style: {
3136
- display: "block",
3137
- backgroundColor: "rgba(0, 0, 0, 0.08)",
3138
- padding: "0.75em",
3139
- borderRadius: "6px",
3140
- fontSize: "0.9em",
3141
- fontFamily: "monospace",
3142
- overflowX: "auto",
3143
- marginTop: "0.5em",
3144
- marginBottom: "0.5em"
3145
- },
3146
- ...props,
3147
- children
3148
- }
3149
- );
3150
- },
3151
- // Pre (code block wrapper)
3152
- pre: ({ children, ...props }) => /* @__PURE__ */ jsx6("pre", { style: { margin: 0 }, ...props, children }),
3153
- // Blockquotes
3154
- blockquote: ({ children, ...props }) => /* @__PURE__ */ jsx6(
3155
- "blockquote",
3156
- {
3157
- style: {
3158
- borderLeft: "4px solid rgba(0, 0, 0, 0.2)",
3159
- paddingLeft: "1em",
3160
- marginLeft: 0,
3161
- marginTop: "0.5em",
3162
- marginBottom: "0.5em",
3163
- color: "rgba(0, 0, 0, 0.7)"
3164
- },
3165
- ...props,
3166
- children
3167
- }
3168
- ),
3169
- // Strong/bold
3170
- strong: ({ children, ...props }) => /* @__PURE__ */ jsx6("strong", { style: { fontWeight: 600 }, ...props, children }),
3171
- // Emphasis/italic
3172
- em: ({ children, ...props }) => /* @__PURE__ */ jsx6("em", { style: { fontStyle: "italic" }, ...props, children }),
3173
- // Horizontal rule
3174
- hr: (props) => /* @__PURE__ */ jsx6(
3175
- "hr",
3176
- {
3177
- style: {
3178
- border: "none",
3179
- borderTop: "1px solid rgba(0, 0, 0, 0.1)",
3180
- marginTop: "1em",
3181
- marginBottom: "1em"
3182
- },
3183
- ...props
3184
- }
3185
- ),
3186
- // Tables (GFM)
3187
- table: ({ children, ...props }) => /* @__PURE__ */ jsx6("div", { style: { overflowX: "auto", marginTop: "0.5em", marginBottom: "0.5em" }, children: /* @__PURE__ */ jsx6(
3188
- "table",
3189
- {
3190
- style: {
3191
- borderCollapse: "collapse",
3192
- width: "100%",
3193
- fontSize: "0.9em"
3194
- },
3195
- ...props,
3196
- children
3197
- }
3198
- ) }),
3199
- th: ({ children, ...props }) => /* @__PURE__ */ jsx6(
3200
- "th",
3201
- {
3202
- style: {
3203
- border: "1px solid rgba(0, 0, 0, 0.2)",
3204
- padding: "0.5em",
3205
- backgroundColor: "rgba(0, 0, 0, 0.05)",
3206
- fontWeight: 600,
3207
- textAlign: "left"
3208
- },
3209
- ...props,
3210
- children
3211
- }
3212
- ),
3213
- td: ({ children, ...props }) => /* @__PURE__ */ jsx6(
3214
- "td",
3215
- {
3216
- style: {
3217
- border: "1px solid rgba(0, 0, 0, 0.2)",
3218
- padding: "0.5em"
3219
- },
3220
- ...props,
3221
- children
3222
- }
3223
- )
3224
- },
3225
- children: normalizedContent
3226
- }
3227
- ) });
3228
- };
3229
-
3230
- // src/react/ai/FloatingChatBox.tsx
3231
- import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3232
- var ChatIcon = () => /* @__PURE__ */ jsx7("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx7("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
3233
- var CloseIcon = () => /* @__PURE__ */ jsxs6("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3234
- /* @__PURE__ */ jsx7("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
3235
- /* @__PURE__ */ jsx7("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
3236
- ] });
3237
- var ExpandIcon = () => /* @__PURE__ */ jsxs6("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3238
- /* @__PURE__ */ jsx7("polyline", { points: "15 3 21 3 21 9" }),
3239
- /* @__PURE__ */ jsx7("polyline", { points: "9 21 3 21 3 15" }),
3240
- /* @__PURE__ */ jsx7("line", { x1: "21", y1: "3", x2: "14", y2: "10" }),
3241
- /* @__PURE__ */ jsx7("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
3242
- ] });
3243
- var CollapseIcon = () => /* @__PURE__ */ jsxs6("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3244
- /* @__PURE__ */ jsx7("polyline", { points: "4 14 10 14 10 20" }),
3245
- /* @__PURE__ */ jsx7("polyline", { points: "20 10 14 10 14 4" }),
3246
- /* @__PURE__ */ jsx7("line", { x1: "14", y1: "10", x2: "21", y2: "3" }),
3247
- /* @__PURE__ */ jsx7("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
3248
- ] });
3249
- var SendIcon = () => /* @__PURE__ */ jsxs6("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3250
- /* @__PURE__ */ jsx7("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
3251
- /* @__PURE__ */ jsx7("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
3252
- ] });
3253
- var LayersIcon = () => /* @__PURE__ */ jsxs6("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3254
- /* @__PURE__ */ jsx7("polygon", { points: "12 2 2 7 12 12 22 7 12 2" }),
3255
- /* @__PURE__ */ jsx7("polyline", { points: "2 17 12 22 22 17" }),
3256
- /* @__PURE__ */ jsx7("polyline", { points: "2 12 12 17 22 12" })
3257
- ] });
3258
- var styles = {
3259
- root: {
3260
- fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
3261
- },
3262
- // Floating button (closed state - wide with text, open state - circular with X)
3263
- floatingButton: {
3264
- position: "fixed",
3265
- bottom: 16,
3266
- right: 16,
3267
- borderRadius: "999px",
3268
- border: "none",
3269
- cursor: "pointer",
3270
- background: "linear-gradient(135deg, #10b981, #059669)",
3271
- color: "#fff",
3272
- boxShadow: "0 12px 28px rgba(16, 185, 129, 0.4)",
3273
- display: "flex",
3274
- alignItems: "center",
3275
- justifyContent: "center",
3276
- fontSize: 14,
3277
- fontWeight: 600,
3278
- zIndex: 99999,
3279
- transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
3280
- },
3281
- floatingButtonClosed: {
3282
- padding: "12px 20px",
3283
- gap: 8
3284
- },
3285
- floatingButtonOpen: {
3286
- width: 52,
3287
- height: 52,
3288
- padding: 0
3289
- },
3290
- floatingButtonMobile: {
3291
- width: 52,
3292
- height: 52,
3293
- padding: 0
3294
- },
3295
- // Panel (expandable)
3296
- panel: {
3297
- position: "fixed",
3298
- bottom: 80,
3299
- right: 16,
3300
- background: "#fff",
3301
- borderRadius: 16,
3302
- boxShadow: "0 20px 60px rgba(15, 23, 42, 0.3), 0 0 0 1px rgba(15, 23, 42, 0.05)",
3303
- display: "flex",
3304
- flexDirection: "column",
3305
- overflow: "hidden",
3306
- zIndex: 99999,
3307
- transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
3308
- },
3309
- panelNormal: {
3310
- width: 360,
3311
- height: 520
3312
- },
3313
- panelExpanded: {
3314
- width: 480,
3315
- height: 640
3316
- },
3317
- panelMobileFullscreen: {
3318
- position: "fixed",
3319
- top: 0,
3320
- left: 0,
3321
- right: 0,
3322
- bottom: 0,
3323
- width: "100%",
3324
- height: "100%",
3325
- borderRadius: 0,
3326
- margin: 0
3327
- },
3328
- // Header with green gradient
3329
- header: {
3330
- padding: "14px 16px",
3331
- background: "linear-gradient(135deg, #10b981, #059669)",
3332
- color: "#fff",
3333
- display: "flex",
3334
- alignItems: "center",
3335
- justifyContent: "space-between"
3336
- },
3337
- title: {
3338
- margin: 0,
3339
- fontSize: 15,
3340
- fontWeight: 600,
3341
- letterSpacing: "-0.01em"
3342
- },
3343
- headerButtons: {
3344
- display: "flex",
3345
- alignItems: "center",
3346
- gap: 8
3347
- },
3348
- headerButton: {
3349
- border: "none",
3350
- background: "rgba(255, 255, 255, 0.15)",
3351
- color: "#fff",
3352
- width: 30,
3353
- height: 30,
3354
- borderRadius: 8,
3355
- cursor: "pointer",
3356
- display: "flex",
3357
- alignItems: "center",
3358
- justifyContent: "center",
3359
- transition: "background 0.2s"
3360
- },
3361
- // Messages area
3362
- messages: {
3363
- flex: 1,
3364
- padding: "16px",
3365
- overflowY: "auto",
3366
- background: "#f8fafc",
3367
- display: "flex",
3368
- flexDirection: "column",
3369
- gap: 16
3370
- },
3371
- // Message bubbles
3372
- messageWrapper: {
3373
- display: "flex",
3374
- flexDirection: "column",
3375
- gap: 8
3376
- },
3377
- messageBubble: {
3378
- maxWidth: "85%",
3379
- padding: "10px 12px",
3380
- borderRadius: 16,
3381
- lineHeight: 1.5,
3382
- fontSize: 13,
3383
- whiteSpace: "pre-wrap",
3384
- wordBreak: "break-word"
3385
- },
3386
- userMessage: {
3387
- alignSelf: "flex-end",
3388
- background: "linear-gradient(135deg, #10b981, #059669)",
3389
- color: "#fff",
3390
- borderBottomRightRadius: 4
3391
- },
3392
- assistantMessage: {
3393
- alignSelf: "flex-start",
3394
- background: "#e2e8f0",
3395
- color: "#0f172a",
3396
- borderBottomLeftRadius: 4
3397
- },
3398
- // Streaming cursor
3399
- cursor: {
3400
- display: "inline-block",
3401
- width: 2,
3402
- height: 16,
3403
- background: "#0f172a",
3404
- marginLeft: 2,
3405
- animation: "zenitBlink 1s infinite",
3406
- verticalAlign: "text-bottom"
3407
- },
3408
- thinkingText: {
3409
- fontStyle: "italic",
3410
- opacity: 0.7,
3411
- display: "flex",
3412
- alignItems: "center",
3413
- gap: 8
3414
- },
3415
- typingIndicator: {
3416
- display: "flex",
3417
- gap: 4,
3418
- alignItems: "center"
3419
- },
3420
- typingDot: {
3421
- width: 8,
3422
- height: 8,
3423
- borderRadius: "50%",
3424
- backgroundColor: "#059669",
3425
- animation: "typingDotBounce 1.4s infinite ease-in-out"
3426
- },
3427
- // Metadata section (Referenced Layers)
3428
- metadataSection: {
3429
- marginTop: 12,
3430
- padding: "10px 12px",
3431
- background: "rgba(255, 255, 255, 0.5)",
3432
- borderRadius: 10,
3433
- fontSize: 12
3434
- },
3435
- metadataTitle: {
3436
- fontWeight: 600,
3437
- color: "#059669",
3438
- marginBottom: 6,
3439
- display: "flex",
3440
- alignItems: "center",
3441
- gap: 6
3442
- },
3443
- metadataList: {
3444
- margin: 0,
3445
- paddingLeft: 18,
3446
- color: "#475569"
3447
- },
3448
- metadataItem: {
3449
- marginBottom: 2
3450
- },
3451
- // Suggested actions
3452
- actionsSection: {
3453
- marginTop: 12
3454
- },
3455
- sectionLabel: {
3456
- fontSize: 11,
3457
- fontWeight: 600,
3458
- color: "#64748b",
3459
- marginBottom: 8,
3460
- textTransform: "uppercase",
3461
- letterSpacing: "0.05em"
3462
- },
3463
- actionsGrid: {
3464
- display: "flex",
3465
- flexWrap: "wrap",
3466
- gap: 6
3467
- },
3468
- actionButton: {
3469
- border: "1px solid #d1fae5",
3470
- background: "#d1fae5",
3471
- color: "#065f46",
3472
- borderRadius: 999,
3473
- padding: "6px 12px",
3474
- fontSize: 12,
3475
- fontWeight: 500,
3476
- cursor: "pointer",
3477
- transition: "all 0.2s"
3478
- },
3479
- // Follow-up questions
3480
- followUpButton: {
3481
- border: "1px solid #cbd5e1",
3482
- background: "#fff",
3483
- color: "#475569",
3484
- borderRadius: 10,
3485
- padding: "8px 12px",
3486
- fontSize: 12,
3487
- fontWeight: 500,
3488
- cursor: "pointer",
3489
- textAlign: "left",
3490
- width: "100%",
3491
- transition: "all 0.2s"
3492
- },
3493
- // Input area
3494
- inputWrapper: {
3495
- borderTop: "1px solid #e2e8f0",
3496
- padding: "10px 14px",
3497
- display: "flex",
3498
- gap: 10,
3499
- alignItems: "flex-end",
3500
- background: "#fff"
3501
- },
3502
- textarea: {
3503
- flex: 1,
3504
- resize: "none",
3505
- borderRadius: 12,
3506
- border: "1.5px solid #cbd5e1",
3507
- padding: "8px 10px",
3508
- fontSize: 13,
3509
- fontFamily: "inherit",
3510
- lineHeight: 1.4,
3511
- transition: "border-color 0.2s"
3512
- },
3513
- textareaFocus: {
3514
- borderColor: "#10b981",
3515
- outline: "none"
3516
- },
3517
- sendButton: {
3518
- borderRadius: 12,
3519
- border: "none",
3520
- padding: "8px 12px",
3521
- background: "linear-gradient(135deg, #10b981, #059669)",
3522
- color: "#fff",
3523
- cursor: "pointer",
3524
- fontSize: 13,
3525
- fontWeight: 600,
3526
- display: "flex",
3527
- alignItems: "center",
3528
- justifyContent: "center",
3529
- transition: "opacity 0.2s, transform 0.2s",
3530
- minWidth: 40,
3531
- height: 40
3532
- },
3533
- // Status messages
3534
- statusNote: {
3535
- padding: "0 16px 14px",
3536
- fontSize: 12,
3537
- color: "#64748b",
3538
- textAlign: "center"
3539
- },
3540
- errorText: {
3541
- padding: "0 16px 14px",
3542
- fontSize: 12,
3543
- color: "#dc2626",
3544
- textAlign: "center"
3545
- }
3546
- };
3547
- var FloatingChatBox = ({
3548
- mapId,
3549
- filteredLayerIds,
3550
- filters,
3551
- userId,
3552
- baseUrl,
3553
- accessToken,
3554
- getAccessToken,
3555
- onActionClick,
3556
- onOpenChange,
3557
- hideButton,
3558
- open: openProp
3559
- }) => {
3560
- const isControlled = openProp !== void 0;
3561
- const [internalOpen, setInternalOpen] = useState5(false);
3562
- const open = isControlled ? openProp : internalOpen;
3563
- const setOpen = useCallback4((value) => {
3564
- const newValue = typeof value === "function" ? value(open) : value;
3565
- if (!isControlled) {
3566
- setInternalOpen(newValue);
3567
- }
3568
- onOpenChange?.(newValue);
3569
- }, [isControlled, open, onOpenChange]);
3570
- const [expanded, setExpanded] = useState5(false);
3571
- const [messages, setMessages] = useState5([]);
3572
- const [inputValue, setInputValue] = useState5("");
3573
- const [conversationId, setConversationId] = useState5();
3574
- const [errorMessage, setErrorMessage] = useState5(null);
3575
- const [isFocused, setIsFocused] = useState5(false);
3576
- const [isMobile, setIsMobile] = useState5(false);
3577
- const messagesEndRef = useRef8(null);
3578
- const messagesContainerRef = useRef8(null);
3579
- const chatBoxRef = useRef8(null);
3580
- const chatConfig = useMemo4(() => {
3581
- if (!baseUrl) return void 0;
3582
- return { baseUrl, accessToken, getAccessToken };
3583
- }, [accessToken, baseUrl, getAccessToken]);
3584
- const { sendMessage: sendMessage2, isStreaming, streamingText, completeResponse } = useSendMessageStream(chatConfig);
3585
- const canSend = Boolean(mapId) && Boolean(baseUrl) && inputValue.trim().length > 0 && !isStreaming;
3586
- useEffect7(() => {
3587
- if (open && isMobile) {
3588
- setExpanded(true);
3589
- }
3590
- }, [open, isMobile]);
3591
- const scrollToBottom = useCallback4(() => {
3592
- if (messagesEndRef.current) {
3593
- messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
3594
- }
3595
- }, []);
3596
- useEffect7(() => {
3597
- if (open && messages.length === 0) {
3598
- setMessages([
3599
- {
3600
- id: "welcome",
3601
- role: "assistant",
3602
- content: "Hola, soy el asistente de IA de ZENIT. Estoy aqu\xED para ayudarte a analizar tu mapa y responder tus preguntas sobre las capas y datos. \xBFEn qu\xE9 puedo ayudarte hoy?"
3603
- }
3604
- ]);
3605
- }
3606
- }, [open, messages.length]);
3607
- useEffect7(() => {
3608
- scrollToBottom();
3609
- }, [messages, streamingText, scrollToBottom]);
3610
- useEffect7(() => {
3611
- if (!open) return;
3612
- if (isMobile && expanded) return;
3613
- const handleClickOutside = (event) => {
3614
- if (chatBoxRef.current && !chatBoxRef.current.contains(event.target)) {
3615
- setOpen(false);
3616
- }
3617
- };
3618
- document.addEventListener("mousedown", handleClickOutside);
3619
- return () => {
3620
- document.removeEventListener("mousedown", handleClickOutside);
3621
- };
3622
- }, [open, isMobile, expanded]);
3623
- useEffect7(() => {
3624
- if (typeof window === "undefined") return;
3625
- const mediaQuery = window.matchMedia("(max-width: 768px)");
3626
- const updateMobile = () => setIsMobile(mediaQuery.matches);
3627
- updateMobile();
3628
- if (mediaQuery.addEventListener) {
3629
- mediaQuery.addEventListener("change", updateMobile);
3630
- } else {
3631
- mediaQuery.addListener(updateMobile);
3632
- }
3633
- return () => {
3634
- if (mediaQuery.removeEventListener) {
3635
- mediaQuery.removeEventListener("change", updateMobile);
3636
- } else {
3637
- mediaQuery.removeListener(updateMobile);
3638
- }
3639
- };
3640
- }, []);
3641
- useEffect7(() => {
3642
- if (typeof document === "undefined") return;
3643
- if (!open || !isMobile) return;
3644
- document.body.style.overflow = "hidden";
3645
- return () => {
3646
- document.body.style.overflow = "";
3647
- };
3648
- }, [open, isMobile]);
3649
- const addMessage = useCallback4((message) => {
3650
- setMessages((prev) => [...prev, message]);
3651
- }, []);
3652
- const handleSend = useCallback4(async () => {
3653
- if (!mapId) {
3654
- setErrorMessage("Selecciona un mapa para usar el asistente.");
3655
- return;
3656
- }
3657
- if (!baseUrl) {
3658
- setErrorMessage("Configura la baseUrl del SDK para usar Zenit AI.");
3659
- return;
3660
- }
3661
- if (!inputValue.trim()) return;
3662
- const messageText = inputValue.trim();
3663
- setInputValue("");
3664
- setErrorMessage(null);
3665
- addMessage({
3666
- id: `user-${Date.now()}`,
3667
- role: "user",
3668
- content: messageText
3669
- });
3670
- const request = {
3671
- message: messageText,
3672
- conversationId,
3673
- filteredLayerIds,
3674
- filters,
3675
- userId
3676
- };
3677
- try {
3678
- const response = await sendMessage2(mapId, request);
3679
- setConversationId(response.conversationId ?? conversationId);
3680
- addMessage({
3681
- id: `assistant-${Date.now()}`,
3682
- role: "assistant",
3683
- content: response.answer,
3684
- response
3685
- });
3686
- } catch (error) {
3687
- setErrorMessage("Ocurri\xF3 un error al generar la respuesta.");
3688
- addMessage({
3689
- id: `error-${Date.now()}`,
3690
- role: "assistant",
3691
- content: "\u274C Ocurri\xF3 un error al generar la respuesta."
3692
- });
3693
- }
3694
- }, [
3695
- addMessage,
3696
- baseUrl,
3697
- conversationId,
3698
- filteredLayerIds,
3699
- filters,
3700
- inputValue,
3701
- mapId,
3702
- sendMessage2,
3703
- userId
3704
- ]);
3705
- const handleKeyDown = useCallback4(
3706
- (event) => {
3707
- if (event.key === "Enter" && !event.shiftKey) {
3708
- event.preventDefault();
3709
- if (canSend) {
3710
- void handleSend();
3711
- }
3712
- }
3713
- },
3714
- [canSend, handleSend]
3715
- );
3716
- const handleFollowUpClick = useCallback4((question) => {
3717
- setInputValue(question);
3718
- }, []);
3719
- const renderMetadata = (response) => {
3720
- if (!response?.metadata) return null;
3721
- const referencedLayers = response.metadata.referencedLayers;
3722
- if (!referencedLayers || referencedLayers.length === 0) return null;
3723
- return /* @__PURE__ */ jsxs6("div", { style: styles.metadataSection, children: [
3724
- /* @__PURE__ */ jsxs6("div", { style: styles.metadataTitle, children: [
3725
- /* @__PURE__ */ jsx7(LayersIcon, {}),
3726
- "Capas Analizadas"
3727
- ] }),
3728
- /* @__PURE__ */ jsx7("ul", { style: styles.metadataList, children: referencedLayers.map((layer, index) => /* @__PURE__ */ jsxs6("li", { style: styles.metadataItem, children: [
3729
- /* @__PURE__ */ jsx7("strong", { children: layer.layerName }),
3730
- " (",
3731
- layer.featureCount,
3732
- " ",
3733
- layer.featureCount === 1 ? "elemento" : "elementos",
3734
- ")"
3735
- ] }, index)) })
3736
- ] });
3737
- };
3738
- const handleActionClick = useCallback4((action) => {
3739
- if (isStreaming) return;
3740
- setOpen(false);
3741
- requestAnimationFrame(() => {
3742
- onActionClick?.(action);
3743
- });
3744
- }, [isStreaming, setOpen, onActionClick]);
3745
- const renderActions = (response) => {
3746
- if (!response?.suggestedActions?.length) return null;
3747
- return /* @__PURE__ */ jsxs6("div", { style: styles.actionsSection, children: [
3748
- /* @__PURE__ */ jsx7("div", { style: styles.sectionLabel, children: "Acciones Sugeridas" }),
3749
- /* @__PURE__ */ jsx7("div", { style: styles.actionsGrid, children: response.suggestedActions.map((action, index) => /* @__PURE__ */ jsx7(
3750
- "button",
3751
- {
3752
- type: "button",
3753
- style: {
3754
- ...styles.actionButton,
3755
- opacity: isStreaming ? 0.5 : 1,
3756
- cursor: isStreaming ? "not-allowed" : "pointer"
3757
- },
3758
- onClick: () => handleActionClick(action),
3759
- disabled: isStreaming,
3760
- onMouseEnter: (e) => {
3761
- if (!isStreaming) {
3762
- e.currentTarget.style.background = "#a7f3d0";
3763
- e.currentTarget.style.borderColor = "#a7f3d0";
3764
- }
3765
- },
3766
- onMouseLeave: (e) => {
3767
- if (!isStreaming) {
3768
- e.currentTarget.style.background = "#d1fae5";
3769
- e.currentTarget.style.borderColor = "#d1fae5";
3770
- }
3771
- },
3772
- children: action.label ?? action.action ?? "Acci\xF3n"
3773
- },
3774
- `${action.label ?? action.action ?? "action"}-${index}`
3775
- )) })
3776
- ] });
3777
- };
3778
- const renderFollowUps = (response) => {
3779
- if (!response?.followUpQuestions?.length) return null;
3780
- return /* @__PURE__ */ jsxs6("div", { style: styles.actionsSection, children: [
3781
- /* @__PURE__ */ jsx7("div", { style: styles.sectionLabel, children: "Preguntas Relacionadas" }),
3782
- /* @__PURE__ */ jsx7("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: response.followUpQuestions.map((question, index) => /* @__PURE__ */ jsx7(
3783
- "button",
3784
- {
3785
- type: "button",
3786
- style: {
3787
- ...styles.followUpButton,
3788
- opacity: isStreaming ? 0.5 : 1,
3789
- cursor: isStreaming ? "not-allowed" : "pointer"
3790
- },
3791
- onClick: () => !isStreaming && handleFollowUpClick(question),
3792
- disabled: isStreaming,
3793
- onMouseEnter: (e) => {
3794
- if (!isStreaming) {
3795
- e.currentTarget.style.background = "#f8fafc";
3796
- e.currentTarget.style.borderColor = "#10b981";
3797
- }
3798
- },
3799
- onMouseLeave: (e) => {
3800
- if (!isStreaming) {
3801
- e.currentTarget.style.background = "#fff";
3802
- e.currentTarget.style.borderColor = "#cbd5e1";
3803
- }
3804
- },
3805
- children: question
3806
- },
3807
- `followup-${index}`
3808
- )) })
3809
- ] });
3810
- };
3811
- const chatContent = /* @__PURE__ */ jsxs6("div", { style: styles.root, children: [
3812
- /* @__PURE__ */ jsx7("style", { children: `
3813
- @keyframes zenitBlink {
3814
- 0%, 49% { opacity: 1; }
3815
- 50%, 100% { opacity: 0; }
3816
- }
3817
- @keyframes zenitPulse {
3818
- 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
3819
- 70% { box-shadow: 0 0 0 14px rgba(16, 185, 129, 0); }
3820
- 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
3821
- }
3822
- @keyframes typingDotBounce {
3823
- 0%, 60%, 100% { transform: translateY(0); }
3824
- 30% { transform: translateY(-8px); }
3825
- }
3826
- .zenit-typing-dot:nth-child(1) { animation-delay: 0s; }
3827
- .zenit-typing-dot:nth-child(2) { animation-delay: 0.2s; }
3828
- .zenit-typing-dot:nth-child(3) { animation-delay: 0.4s; }
3829
- .zenit-ai-button:not(.open) {
3830
- animation: zenitPulse 2.5s infinite;
3831
- }
3832
- .zenit-ai-button:hover {
3833
- transform: scale(1.05);
3834
- }
3835
- .zenit-ai-button:active {
3836
- transform: scale(0.95);
3837
- }
3838
- .zenit-send-button:disabled {
3839
- opacity: 0.5;
3840
- cursor: not-allowed;
3841
- }
3842
- .zenit-send-button:not(:disabled):hover {
3843
- opacity: 0.9;
3844
- transform: translateY(-1px);
3845
- }
3846
- .zenit-send-button:not(:disabled):active {
3847
- transform: translateY(0);
3848
- }
3849
- .zenit-chat-panel {
3850
- box-sizing: border-box;
3851
- }
3852
- @media (max-width: 768px) {
3853
- .zenit-chat-panel.zenit-chat-panel--fullscreen .zenit-ai-body {
3854
- flex: 1;
3855
- min-height: 0;
3856
- overflow-y: auto;
3857
- -webkit-overflow-scrolling: touch;
3858
- }
3859
- .zenit-chat-panel.zenit-chat-panel--fullscreen .zenit-ai-input-area {
3860
- flex-shrink: 0;
3861
- padding-bottom: max(14px, env(safe-area-inset-bottom));
3862
- }
3863
- .zenit-ai-button.zenit-ai-button--hidden-mobile {
3864
- display: none !important;
3865
- }
3866
- }
3867
- ` }),
3868
- open && /* @__PURE__ */ jsxs6(
3869
- "div",
3870
- {
3871
- ref: chatBoxRef,
3872
- className: `zenit-chat-panel${expanded && !isMobile ? " zenit-chat-panel--expanded" : ""}${isMobile ? " zenit-chat-panel--fullscreen" : ""}`,
3873
- style: (() => {
3874
- const desktopStyle = expanded ? styles.panelExpanded : styles.panelNormal;
3875
- const mobileStyle = styles.panelMobileFullscreen;
3876
- return {
3877
- ...styles.panel,
3878
- ...isMobile ? mobileStyle : desktopStyle
3879
- };
3880
- })(),
3881
- children: [
3882
- /* @__PURE__ */ jsxs6("header", { style: styles.header, children: [
3883
- /* @__PURE__ */ jsx7("h3", { style: styles.title, children: "Asistente Zenit AI" }),
3884
- /* @__PURE__ */ jsxs6("div", { style: styles.headerButtons, children: [
3885
- !isMobile && /* @__PURE__ */ jsx7(
3886
- "button",
3887
- {
3888
- type: "button",
3889
- style: styles.headerButton,
3890
- onClick: () => setExpanded(!expanded),
3891
- onMouseEnter: (e) => {
3892
- e.currentTarget.style.background = "rgba(255, 255, 255, 0.25)";
3893
- },
3894
- onMouseLeave: (e) => {
3895
- e.currentTarget.style.background = "rgba(255, 255, 255, 0.15)";
3896
- },
3897
- "aria-label": expanded ? "Contraer" : "Expandir",
3898
- children: expanded ? /* @__PURE__ */ jsx7(CollapseIcon, {}) : /* @__PURE__ */ jsx7(ExpandIcon, {})
3899
- }
3900
- ),
3901
- /* @__PURE__ */ jsx7(
3902
- "button",
3903
- {
3904
- type: "button",
3905
- style: styles.headerButton,
3906
- onClick: () => setOpen(false),
3907
- onMouseEnter: (e) => {
3908
- e.currentTarget.style.background = "rgba(255, 255, 255, 0.25)";
3909
- },
3910
- onMouseLeave: (e) => {
3911
- e.currentTarget.style.background = "rgba(255, 255, 255, 0.15)";
3912
- },
3913
- "aria-label": "Cerrar",
3914
- children: /* @__PURE__ */ jsx7(CloseIcon, {})
3915
- }
3916
- )
3917
- ] })
3918
- ] }),
3919
- /* @__PURE__ */ jsxs6("div", { ref: messagesContainerRef, className: "zenit-ai-body", style: styles.messages, children: [
3920
- messages.map((message) => /* @__PURE__ */ jsx7(
3921
- "div",
3922
- {
3923
- style: {
3924
- ...styles.messageWrapper,
3925
- alignItems: message.role === "user" ? "flex-end" : "flex-start"
3926
- },
3927
- children: /* @__PURE__ */ jsxs6(
3928
- "div",
3929
- {
3930
- style: {
3931
- ...styles.messageBubble,
3932
- ...message.role === "user" ? styles.userMessage : styles.assistantMessage
3933
- },
3934
- children: [
3935
- message.role === "assistant" ? /* @__PURE__ */ jsx7(MarkdownRenderer, { content: message.content }) : message.content,
3936
- message.role === "assistant" && renderMetadata(message.response),
3937
- message.role === "assistant" && renderActions(message.response),
3938
- message.role === "assistant" && renderFollowUps(message.response)
3939
- ]
3940
- }
3941
- )
3942
- },
3943
- message.id
3944
- )),
3945
- isStreaming && /* @__PURE__ */ jsx7(
3946
- "div",
3947
- {
3948
- style: {
3949
- ...styles.messageWrapper,
3950
- alignItems: "flex-start"
3951
- },
3952
- children: /* @__PURE__ */ jsx7(
3953
- "div",
3954
- {
3955
- style: {
3956
- ...styles.messageBubble,
3957
- ...styles.assistantMessage
3958
- },
3959
- children: streamingText ? /* @__PURE__ */ jsxs6(Fragment4, { children: [
3960
- /* @__PURE__ */ jsx7(MarkdownRenderer, { content: streamingText }),
3961
- /* @__PURE__ */ jsx7("span", { style: styles.cursor })
3962
- ] }) : /* @__PURE__ */ jsxs6("div", { style: styles.thinkingText, children: [
3963
- /* @__PURE__ */ jsx7("span", { children: "Pensando" }),
3964
- /* @__PURE__ */ jsxs6("div", { style: styles.typingIndicator, children: [
3965
- /* @__PURE__ */ jsx7("div", { className: "zenit-typing-dot", style: styles.typingDot }),
3966
- /* @__PURE__ */ jsx7("div", { className: "zenit-typing-dot", style: styles.typingDot }),
3967
- /* @__PURE__ */ jsx7("div", { className: "zenit-typing-dot", style: styles.typingDot })
3968
- ] })
3969
- ] })
3970
- }
3971
- )
3972
- }
3973
- ),
3974
- /* @__PURE__ */ jsx7("div", { ref: messagesEndRef })
3975
- ] }),
3976
- /* @__PURE__ */ jsxs6("div", { className: "zenit-ai-input-area", style: styles.inputWrapper, children: [
3977
- /* @__PURE__ */ jsx7(
3978
- "textarea",
3979
- {
3980
- style: {
3981
- ...styles.textarea,
3982
- ...isFocused ? styles.textareaFocus : {}
3983
- },
3984
- rows: 2,
3985
- value: inputValue,
3986
- onChange: (event) => setInputValue(event.target.value),
3987
- onKeyDown: handleKeyDown,
3988
- onFocus: () => setIsFocused(true),
3989
- onBlur: () => setIsFocused(false),
3990
- placeholder: !mapId ? "Selecciona un mapa para comenzar" : isStreaming ? "Esperando respuesta..." : "Escribe tu pregunta...",
3991
- disabled: !mapId || !baseUrl || isStreaming
3992
- }
3993
- ),
3994
- /* @__PURE__ */ jsx7(
3995
- "button",
3996
- {
3997
- type: "button",
3998
- className: "zenit-send-button",
3999
- style: styles.sendButton,
4000
- onClick: () => void handleSend(),
4001
- disabled: !canSend,
4002
- "aria-label": "Enviar mensaje",
4003
- children: /* @__PURE__ */ jsx7(SendIcon, {})
4004
- }
4005
- )
4006
- ] }),
4007
- errorMessage && /* @__PURE__ */ jsx7("div", { style: styles.errorText, children: errorMessage }),
4008
- isStreaming && !errorMessage && /* @__PURE__ */ jsx7("div", { style: styles.statusNote, children: "Generando sugerencias..." }),
4009
- !mapId && !errorMessage && /* @__PURE__ */ jsx7("div", { style: styles.statusNote, children: "Selecciona un mapa para usar el asistente" }),
4010
- !baseUrl && !errorMessage && /* @__PURE__ */ jsx7("div", { style: styles.statusNote, children: "Configura la baseUrl del SDK" })
4011
- ]
4012
- }
4013
- ),
4014
- !(hideButton && !open) && /* @__PURE__ */ jsx7(
4015
- "button",
4016
- {
4017
- type: "button",
4018
- className: `zenit-ai-button ${open ? "open" : ""}${open && isMobile ? " zenit-ai-button--hidden-mobile" : ""}`,
4019
- style: {
4020
- ...styles.floatingButton,
4021
- ...open ? styles.floatingButtonOpen : isMobile ? styles.floatingButtonMobile : styles.floatingButtonClosed,
4022
- zIndex: open ? 100001 : 99999
4023
- },
4024
- onClick: () => setOpen((prev) => !prev),
4025
- "aria-label": open ? "Cerrar asistente" : "Abrir asistente Zenit AI",
4026
- children: open ? /* @__PURE__ */ jsx7(CloseIcon, {}) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
4027
- /* @__PURE__ */ jsx7(ChatIcon, {}),
4028
- !isMobile && /* @__PURE__ */ jsx7("span", { children: "Asistente IA" })
4029
- ] })
4030
- }
4031
- )
4032
- ] });
4033
- if (typeof document !== "undefined") {
4034
- return createPortal2(chatContent, document.body);
4035
- }
4036
- return chatContent;
4037
- };
4038
-
4039
- export {
4040
- extractMapDto,
4041
- normalizeMapCenter,
4042
- normalizeMapLayers,
4043
- computeBBoxFromFeature,
4044
- centroidFromBBox,
4045
- applyFilteredGeoJSONStrategy,
4046
- initLayerStates,
4047
- applyLayerOverrides,
4048
- resetOverrides,
4049
- sendMessage,
4050
- sendMessageStream,
4051
- createChatService,
4052
- resolveLayerAccent,
4053
- getLayerColor,
4054
- getStyleByLayerId,
4055
- getAccentByLayerId,
4056
- clampNumber,
4057
- clampOpacity3 as clampOpacity,
4058
- isPolygonLayer,
4059
- getZoomOpacityFactor,
4060
- getLayerZoomOpacityFactor,
4061
- getEffectiveLayerOpacity,
4062
- ZenitMap,
4063
- Eye,
4064
- EyeOff,
4065
- ChevronLeft,
4066
- ChevronRight,
4067
- Layers,
4068
- Upload,
4069
- X,
4070
- ZoomIn,
4071
- ZenitLayerManager,
4072
- ZenitFeatureFilterPanel,
4073
- useSendMessage,
4074
- useSendMessageStream,
4075
- FloatingChatBox
4076
- };
4077
- //# sourceMappingURL=chunk-URDEEWUZ.mjs.map