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