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