window.initBrilloMapa = function (dotNetHelper) { document.addEventListener("keydown", function (e) { if (e.key && e.key.toLowerCase() === "m") { dotNetHelper.invokeMethodAsync("AlternarBrillo"); } }); }; window.initAltInspector = function (dotNetHelper) { const setInspectorActive = function (active) { window.rt = window.rt || {}; window.rt._inspectorActive = !!active; dotNetHelper.invokeMethodAsync("SetInspectorActivo", !!active); }; document.addEventListener("keydown", function (e) { if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey && (e.key === "Z" || e.key === "z")) { setInspectorActive(true); } }, true); document.addEventListener("keyup", function (e) { if (e.key === "Z" || e.key === "z") setInspectorActive(false); }, true); window.addEventListener("blur", function () { setInspectorActive(false); }); }; window.initZonasShortcut = function (dotNetHelper) { if (window.rt && window.rt._zonasShortcutInicializado) return; window.rt = window.rt || {}; window.rt._zonasShortcutInicializado = true; document.addEventListener("keydown", function (e) { if (e.repeat || e.code !== "KeyE" || !e.ctrlKey || !e.shiftKey || e.altKey || e.metaKey) return; e.preventDefault(); e.stopImmediatePropagation(); dotNetHelper.invokeMethodAsync("AlternarHerramientaZonas").catch(function () { }); }, true); }; window.initFabricaShortcut = function (dotNetHelper) { if (window.rt && window.rt._fabricaShortcutInicializado) return; window.rt = window.rt || {}; window.rt._fabricaShortcutInicializado = true; document.addEventListener("keydown", function (e) { if (e.repeat || e.code !== "KeyF" || !e.ctrlKey || !e.shiftKey || e.altKey || e.metaKey) return; e.preventDefault(); e.stopImmediatePropagation(); document.activeElement?.blur(); dotNetHelper.invokeMethodAsync("GuardarConfiguracionFabricaDesdeAtajoAsync").catch(function () { }); }, true); }; // ----------------------------------------------------------------------------- // Leaflet helpers (sin flechas) para RealTimeMap // ----------------------------------------------------------------------------- (function () { function hookLeaflet(L) { if (!L || window.__leafletHooked) return; // Hook SUPER temprano: constructor del mapa const originalInitialize = L.Map && L.Map.prototype && L.Map.prototype.initialize; if (typeof originalInitialize === "function") { L.Map.prototype.initialize = function (...args) { const res = originalInitialize.apply(this, args); window.__rtLeafletMap = this; // guardamos el mapa creado return res; }; } // Hook adicional: factory L.map (por si acaso) const originalMapFactory = L.map; if (typeof originalMapFactory === "function") { L.map = function (...args) { const map = originalMapFactory.apply(this, args); window.__rtLeafletMap = map; return map; }; } window.__leafletHooked = true; console.log("[rt] Leaflet hook OK"); } // Si Leaflet ya existe: if (window.L) { hookLeaflet(window.L); return; } // Si Leaflet se asigna más tarde: definimos setter let _L = null; Object.defineProperty(window, "L", { configurable: true, enumerable: true, get() { return _L; }, set(v) { _L = v; // restauramos propiedad normal try { delete window.L; window.L = v; } catch { /* ignore */ } hookLeaflet(v); } }); })(); window.rt = window.rt || {}; window.rt.handleInspectorLayerClick = function (event) { if (!window.rt._inspectorActive) return false; const originalEvent = event && event.originalEvent; if (originalEvent) { if (originalEvent.preventDefault) originalEvent.preventDefault(); if (originalEvent.stopPropagation) originalEvent.stopPropagation(); if (originalEvent.stopImmediatePropagation) originalEvent.stopImmediatePropagation(); } const alreadyHandled = !!( (event && event.__rtInspectorHandled) || (originalEvent && originalEvent.__rtInspectorHandled)); if (event) event.__rtInspectorHandled = true; if (originalEvent) originalEvent.__rtInspectorHandled = true; if (!alreadyHandled && window.rt._dotNetHelper) { const latLng = event && event.latlng ? event.latlng : event && event.target && typeof event.target.getLatLng === "function" ? event.target.getLatLng() : null; const lat = Number(latLng && latLng.lat); const lon = Number(latLng && latLng.lng); if (Number.isFinite(lat) && Number.isFinite(lon)) { window.rt._dotNetHelper.invokeMethodAsync("OnInspectorOverlayClick", lat, lon); } } return true; }; window.rt.ensureRouteLayer = function () { const map = window.__rtLeafletMap; if (!map || !window.L) { // ayuda para diagnosticar // console.log("[rt] no map yet", { hasL: !!window.L, hasMap: !!map }); return false; } if (!window.rt._routeLayer) { window.rt._routeLayer = window.L.layerGroup().addTo(map); } if (!window.rt._routeLines) window.rt._routeLines = {}; if (!window.rt._routeCoords) window.rt._routeCoords = {}; return true; }; window.rt.clearElevationSegmentSelection = function () { const selected = window.rt._selectedElevationSegment; if (!selected) return; try { if (selected.line && selected.originalStyle) { selected.line.setStyle(selected.originalStyle); } if (selected.group && Array.isArray(selected.endpoints)) { selected.endpoints.forEach(function (endpoint) { selected.group.removeLayer(endpoint); }); } if (selected.group && Array.isArray(selected.halos)) { selected.halos.forEach(function (halo) { selected.group.removeLayer(halo); }); } } catch { } window.rt._selectedElevationSegment = null; }; window.rt.ensureElevationSegmentSelectionDismiss = function () { if (window.rt._elevationSelectionDismissHandler) return; window.rt._elevationSelectionDismissHandler = function (event) { const selected = window.rt._selectedElevationSegment; if (!selected) return; const selectedPath = selected.line && selected.line._path; const target = event && event.target; if (selectedPath && target && (target === selectedPath || (typeof selectedPath.contains === "function" && selectedPath.contains(target)))) { return; } window.rt.clearElevationSegmentSelection(); }; document.addEventListener( "click", window.rt._elevationSelectionDismissHandler, true); }; window.rt.ensureElevationSegmentSelectionDismiss(); window.rt.selectElevationSegment = function ( routeId, line, coords, group, originalStyle, baseWeight) { window.rt.clearElevationSegmentSelection(); if (!line || !Array.isArray(coords) || coords.length < 2 || !group) return; const selectionColor = originalStyle && originalStyle.color ? originalStyle.color : "#dc2626"; const selectedWeight = Math.max(6, Number(baseWeight || 4) + 2); const outerHalo = window.L.polyline(coords, { color: "#111827", weight: selectedWeight + 7, opacity: 0.94, dashArray: null, lineCap: "round", interactive: false, className: "rt-elev-segment-halo rt-elev-segment-halo--outer" }); const innerHalo = window.L.polyline(coords, { color: "#f8fafc", weight: selectedWeight + 4, opacity: 0.96, dashArray: null, lineCap: "round", interactive: false, className: "rt-elev-segment-halo rt-elev-segment-halo--inner" }); outerHalo.addTo(group); innerHalo.addTo(group); line.setStyle({ color: selectionColor, weight: selectedWeight, opacity: 1, dashArray: null, lineCap: "round" }); const endpointOptions = { radius: 4, color: "#ffffff", weight: 2, opacity: 1, fillColor: selectionColor, fillOpacity: 1, interactive: false, className: "rt-elev-segment-endpoint" }; const endpoints = [ window.L.circleMarker(coords[0], endpointOptions), window.L.circleMarker(coords[coords.length - 1], endpointOptions) ]; endpoints.forEach(function (endpoint) { endpoint.addTo(group); }); try { line.bringToFront(); } catch { } endpoints.forEach(function (endpoint) { try { endpoint.bringToFront(); } catch { } }); window.rt._selectedElevationSegment = { routeId, line, group, halos: [outerHalo, innerHalo], endpoints, originalStyle }; }; window.rt.setElevationSegmentSurface = function (routeId, segmentIndex, surfaceText) { const linesByRoute = window.rt._elevationSegmentLines || {}; const line = linesByRoute[routeId] && linesByRoute[routeId][segmentIndex]; if (!line || !line._rtElevationSegment || typeof line._rtElevationTooltip !== "function") return; line._rtElevationSegment.surfaceText = surfaceText || "No disponible"; const progress = Number.isFinite(line._rtElevationProgress) ? line._rtElevationProgress : 0.5; try { line.setTooltipContent(line._rtElevationTooltip(line._rtElevationSegment, progress)); if (window.rt._selectedElevationSegment && window.rt._selectedElevationSegment.line === line) { line.openTooltip(); } } catch { } }; window.rt.clearRoutes = function () { if (!window.rt.ensureRouteLayer()) return; window.rt.clearElevationSegmentSelection(); window.rt._routeLayer.clearLayers(); window.rt._routeLines = {}; window.rt._routeCoords = {}; window.rt._elevationSegmentLines = {}; if (window.rt._routeStopsLayer) { window.rt._routeStopsLayer.clearLayers(); window.rt._routeStopGroups = {}; } }; window.rt.drawRoute = function (id, coords, options) { if (!window.rt.ensureRouteLayer()) return; if (window.rt._selectedElevationSegment && window.rt._selectedElevationSegment.routeId === id) { window.rt.clearElevationSegmentSelection(); } if (window.rt._routeLines[id]) { window.rt._routeLayer.removeLayer(window.rt._routeLines[id]); delete window.rt._routeLines[id]; } window.rt._elevationSegmentLines = window.rt._elevationSegmentLines || {}; delete window.rt._elevationSegmentLines[id]; const arrowId = id + "__arrow"; if (window.rt._routeLines[arrowId]) { window.rt._routeLayer.removeLayer(window.rt._routeLines[arrowId]); delete window.rt._routeLines[arrowId]; } const routeCoords = Array.isArray(coords) ? coords .map(function (point) { return [Number(point && point[0]), Number(point && point[1])]; }) .filter(function (point) { return Number.isFinite(point[0]) && Number.isFinite(point[1]); }) : []; if (routeCoords.length > 0) window.rt._routeCoords[id] = routeCoords; else delete window.rt._routeCoords[id]; const color = (options && options.color) || "#000"; const weight = (options && options.weight) || 4; const opacity = (options && options.opacity) || 0.9; const dashArray = options && options.dashArray; const lineCap = options && options.lineCap; const arrowEnd = !!(options && options.arrowEnd); const haloColor = options && options.haloColor; const haloWeight = Number(options && options.haloWeight); const haloOpacity = Number(options && options.haloOpacity); const hasHalo = typeof haloColor === "string" && haloColor.length > 0 && Number.isFinite(haloWeight) && haloWeight > weight; const elevationSegments = Array.isArray(options && options.elevationSegments) ? options.elevationSegments : []; const attachRouteClick = function (line) { line.on("click", function (e) { try { if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(e)) return; if (e && e.originalEvent) { if (e.originalEvent.preventDefault) e.originalEvent.preventDefault(); if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation(); if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation(); } if (window.rt._dotNetHelper) { window.rt._dotNetHelper.invokeMethodAsync("OnPreviewItinerarioClick", id); } } catch (e) { } }); }; const buildLine = function (lineCoords, lineOpts) { const line = window.L.polyline(lineCoords, lineOpts); attachRouteClick(line); return line; }; let arrowColor = color; let hasElevationPaint = false; if (elevationSegments.length > 0 && coords && coords.length >= 2) { const group = window.L.layerGroup(); window.rt._elevationSegmentLines = window.rt._elevationSegmentLines || {}; window.rt._elevationSegmentLines[id] = {}; const progressAlongSegment = function (segmentCoords, latlng) { if (!latlng || !Array.isArray(segmentCoords) || segmentCoords.length < 2) return 0.5; const start = segmentCoords[0]; const end = segmentCoords[segmentCoords.length - 1]; const startLat = Number(start && start[0]); const startLon = Number(start && start[1]); const endLat = Number(end && end[0]); const endLon = Number(end && end[1]); if (![startLat, startLon, endLat, endLon, latlng.lat, latlng.lng].every(Number.isFinite)) return 0.5; const lonScale = Math.cos(((startLat + endLat) / 2) * Math.PI / 180); const dx = (endLon - startLon) * lonScale; const dy = endLat - startLat; const px = (latlng.lng - startLon) * lonScale; const py = latlng.lat - startLat; const lengthSquared = dx * dx + dy * dy; if (lengthSquared <= Number.EPSILON) return 0.5; const progress = (px * dx + py * dy) / lengthSquared; return Math.max(0, Math.min(1, progress)); }; const elevationTooltip = function (segment, progress) { const t = Number.isFinite(progress) ? Math.max(0, Math.min(1, progress)) : 0.5; const slope = segment && typeof segment.slope === "number" ? segment.slope : null; const distance = segment && typeof segment.distance === "number" ? segment.distance : null; const elevationStart = segment && typeof segment.elevationStart === "number" ? segment.elevationStart : null; const elevationEnd = segment && typeof segment.elevationEnd === "number" ? segment.elevationEnd : null; const slopeText = slope !== null && Number.isFinite(slope) ? ((slope > 0 ? "+" : "") + slope.toFixed(1) + " %") : (segment && segment.label ? String(segment.label) : "-"); const distanceText = distance !== null && Number.isFinite(distance) ? (distance * t).toFixed(2) + " m" : "-"; const estimatedElevation = elevationStart !== null && Number.isFinite(elevationStart) && elevationEnd !== null && Number.isFinite(elevationEnd) ? elevationStart + (elevationEnd - elevationStart) * t : null; const elevationText = estimatedElevation !== null ? estimatedElevation.toFixed(2) + " m" : "-"; const tooltipLines = [ "Pendiente: " + slopeText, "Dist. anterior: " + distanceText, "Altura: " + elevationText ]; if (segment && segment.surfaceText) { tooltipLines.push("Superficie (V2): " + String(segment.surfaceText)); tooltipLines.push("LiDAR: No disponible (falta la nube clasificada local)"); } return tooltipLines.join("
"); }; elevationSegments.forEach(function (segment, segmentIndex) { const segmentCoords = segment && segment.coords; if (!Array.isArray(segmentCoords) || segmentCoords.length < 2) return; const segmentColor = (segment && segment.color) || color; arrowColor = segmentColor; const segmentOpts = { color: segmentColor, weight, opacity, lineCap: lineCap || "round", className: "rt-elev-segment", bubblingMouseEvents: false }; if (dashArray) segmentOpts.dashArray = dashArray; if (hasHalo) { const haloOpts = { color: haloColor, weight: haloWeight, opacity: Number.isFinite(haloOpacity) ? haloOpacity : opacity, lineCap: lineCap || "round", interactive: false, bubblingMouseEvents: false }; if (dashArray) haloOpts.dashArray = dashArray; window.L.polyline(segmentCoords, haloOpts).addTo(group); } const segmentLine = buildLine(segmentCoords, segmentOpts); segmentLine._rtElevationSegment = segment; segmentLine._rtElevationTooltip = elevationTooltip; segmentLine._rtElevationProgress = 0.5; window.rt._elevationSegmentLines[id][segmentIndex] = segmentLine; const originalSegmentStyle = { color: segmentColor, weight, opacity, lineCap: lineCap || "round", dashArray: dashArray || null }; const label = elevationTooltip(segment, 0.5); if (label) { segmentLine.bindTooltip(label, { sticky: true, direction: "top", opacity: 0.92, className: "rt-elev-tooltip" }); const updateTooltipAtCursor = function (e) { const progress = progressAlongSegment(segmentCoords, e && e.latlng); segmentLine._rtElevationProgress = progress; segmentLine.setTooltipContent(elevationTooltip(segment, progress)); }; segmentLine.on("mouseover", updateTooltipAtCursor); segmentLine.on("mousemove", updateTooltipAtCursor); segmentLine.on("click", function (e) { if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(e)) return; window.rt.selectElevationSegment( id, segmentLine, segmentCoords, group, originalSegmentStyle, weight); segment.surfaceText = "Consultando…"; updateTooltipAtCursor(e); try { segmentLine.setTooltipContent( elevationTooltip(segment, segmentLine._rtElevationProgress)); } catch { } if (window.rt._dotNetHelper) { window.rt._dotNetHelper.invokeMethodAsync( "OnElevationSegmentSelected", id, segmentIndex, segmentCoords).catch(function () { }); } window.requestAnimationFrame(function () { try { if (segmentLine._path && typeof segmentLine._path.blur === "function") { segmentLine._path.blur(); } } catch { } }); if (!window.rt.isMobileLayout || !window.rt.isMobileLayout()) return; updateTooltipAtCursor(e); try { segmentLine.openTooltip(e && e.latlng); } catch { } }); } segmentLine.addTo(group); hasElevationPaint = true; }); if (hasElevationPaint) { group.addTo(window.rt._routeLayer); window.rt._routeLines[id] = group; } } if (!hasElevationPaint) { const lineOpts = { color, weight, opacity }; if (dashArray) lineOpts.dashArray = dashArray; if (lineCap) lineOpts.lineCap = lineCap; const line = buildLine(coords, lineOpts); if (hasHalo) { const group = window.L.layerGroup(); const haloOpts = { color: haloColor, weight: haloWeight, opacity: Number.isFinite(haloOpacity) ? haloOpacity : opacity, lineCap: lineCap || "round", interactive: false, bubblingMouseEvents: false }; if (dashArray) haloOpts.dashArray = dashArray; window.L.polyline(coords, haloOpts).addTo(group); line.addTo(group); group.addTo(window.rt._routeLayer); window.rt._routeLines[id] = group; } else { line.addTo(window.rt._routeLayer); window.rt._routeLines[id] = line; } } if (arrowEnd && coords && coords.length >= 2) { const a = coords[coords.length - 2]; const b = coords[coords.length - 1]; // bearing aprox (en grados) const lat1 = a[0] * Math.PI / 180, lon1 = a[1] * Math.PI / 180; const lat2 = b[0] * Math.PI / 180, lon2 = b[1] * Math.PI / 180; const y = Math.sin(lon2 - lon1) * Math.cos(lat2); const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); let brng = Math.atan2(y, x) * 180 / Math.PI; brng = (brng + 360) % 360; const size = Math.max(14, Math.min(28, weight * 4)); // tamaño ligado al grosor const arrowClass = dashArray ? "rt-arrow rt-arrow--dashed" : "rt-arrow"; const arrowInnerHtml = dashArray ? '' : ''; const icon = window.L.divIcon({ className: "rt-arrow-icon", html: `
${arrowInnerHtml}
`, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); const marker = window.L.marker(b, { icon, interactive: false }); marker.addTo(window.rt._routeLayer); window.rt._routeLines[arrowId] = marker; } }; window.rt.fitRoutes = function (coords, preferredCenter) { const map = window.__rtLeafletMap; if (!map || !window.L) return false; const routePoints = []; if (Array.isArray(coords)) { coords.forEach(function (point) { routePoints.push(point); }); } else { if (!window.rt._routeCoords) return false; Object.values(window.rt._routeCoords).forEach(function (routeCoords) { if (!Array.isArray(routeCoords)) return; routeCoords.forEach(function (point) { routePoints.push(point); }); }); } const points = routePoints .filter(function (point) { return Array.isArray(point) && point.length >= 2 && Number.isFinite(Number(point[0])) && Number.isFinite(Number(point[1])); }) .map(function (point) { return [Number(point[0]), Number(point[1])]; }); if (points.length === 0) return false; const visibleBounds = map.getBounds(); const algunPuntoVisible = visibleBounds && points.some(function (point) { return visibleBounds.contains(window.L.latLng(point[0], point[1])); }); if (algunPuntoVisible) return false; const geometryBounds = window.L.latLngBounds(points); if (!geometryBounds.isValid()) return false; const center = Array.isArray(preferredCenter) && preferredCenter.length >= 2 && Number.isFinite(Number(preferredCenter[0])) && Number.isFinite(Number(preferredCenter[1])) ? window.L.latLng(Number(preferredCenter[0]), Number(preferredCenter[1])) : geometryBounds.getCenter(); map.panTo(center, { animate: true, duration: 0.35 }); return true; }; window.rt = window.rt || {}; window.rt.setZoomLimits = function (minZoom, maxZoom) { const map = window.__rtLeafletMap; if (!map || !window.L) return; map.options.minZoom = minZoom; map.options.maxZoom = maxZoom; map.setMinZoom(minZoom); map.setMaxZoom(maxZoom); map.eachLayer(l => { if (!l) return; // límites generales if (typeof l.setMaxZoom === "function") l.setMaxZoom(maxZoom); if (typeof l.setMinZoom === "function") l.setMinZoom(minZoom); // ✅ SOLO TileLayer if (l instanceof window.L.TileLayer) { l.options.maxNativeZoom = 18; // tiles reales hasta 18 l.options.maxZoom = maxZoom; // permitimos 19 try { l.redraw(); } catch (e) { } } }); try { map.invalidateSize(); } catch (e) { } if (window.rt.addZoomIndicator) window.rt.addZoomIndicator(); }; window.rt = window.rt || {}; // guardar referencia a Blazor para callbacks JS -> .NET window.rt.setDotNetHelper = function (dotNetHelper) { window.rt._dotNetHelper = dotNetHelper; }; // Las zonas son independientes de las capas de rutas; cerrar el editor retira sus eventos. window.rt._exclusionEditMode = false; window.rt._exclusionPolygonLayer = null; window.rt._exclusionClickHandler = null; window.rt._exclusionMapContainer = null; window.rt.clearExclusionPolygonEditor = function () { if (window.rt._exclusionMapContainer && window.rt._exclusionClickHandler) window.rt._exclusionMapContainer.removeEventListener("click", window.rt._exclusionClickHandler, true); window.rt._exclusionPolygonLayer?.remove(); window.rt._exclusionPolygonLayer = null; window.rt._exclusionClickHandler = null; window.rt._exclusionMapContainer = null; window.rt._exclusionEditMode = false; }; window.rt.updateExclusionPolygonEditor = function (zones, selectedId, busy) { window.rt.clearExclusionPolygonEditor(); const map = window.__rtLeafletMap; const L = window.L; if (!map || !L) return; const layer = L.layerGroup().addTo(map); window.rt._exclusionPolygonLayer = layer; const drawing = !busy && zones.some(zone => zone.id === selectedId && !zone.closed); window.rt._exclusionEditMode = drawing; for (const zone of zones) { const selected = zone.id === selectedId; const points = zone.points.map(p => [p[0], p[1]]); const options = { color: zone.active === false ? "#9ca3af" : selected ? "#e879f9" : "#f97316", weight: selected ? 3 : 2, dashArray: zone.active === false ? "4 5" : null, fillOpacity: zone.active === false ? 0.03 : zone.closed ? 0.18 : 0.08, interactive: false }; const polygon = zone.point ? (points.length ? L.circleMarker(points[0], { ...options, radius: 6, fillOpacity: .5 }).addTo(layer) : null) : (zone.closed ? L.polygon(points, options) : L.polyline(points, options)).addTo(layer); if (!selected || busy) continue; points.forEach((point, index) => { const marker = L.marker(point, { draggable: true, autoPan: true, bubblingMouseEvents: false, zIndexOffset: 3000, icon: L.divIcon({ className: "rt-exclusion-vertex", html: "", iconSize: [10, 10], iconAnchor: [5, 5] }) }).addTo(layer); const update = () => { const pos = marker.getLatLng(); points[index] = [pos.lat, pos.lng]; if (zone.point) polygon?.setLatLng(points[0]); else polygon.setLatLngs(points); return pos; }; marker.on("drag", update); marker.on("dragend", () => { const pos = update(); window.rt._dotNetHelper?.invokeMethodAsync( "OnExclusionPolygonVertexDragged", zone.id, index, pos.lat, pos.lng); }); }); } // El clic llega por el evento normal del mapa. No se registra otro manejador // aquí para evitar duplicar vértices, especialmente en las exclusiones puntuales. }; window.rt.isExclusionPolygonValid = function (points) { if (!Array.isArray(points) || points.length < 3) return false; const eps = 1e-12; const cross = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); const on = (a, b, p) => Math.abs(cross(a, b, p)) < eps && p[0] >= Math.min(a[0], b[0]) - eps && p[0] <= Math.max(a[0], b[0]) + eps && p[1] >= Math.min(a[1], b[1]) - eps && p[1] <= Math.max(a[1], b[1]) + eps; const n = points.length; let area = 0; for (let i = 0; i < n; i++) { const a = points[i], b = points[(i + 1) % n]; if (!a || a.length < 2 || !a.every(Number.isFinite)) return false; if (a[0] === b[0] && a[1] === b[1]) return false; area += cross(points[0], a, b); for (let j = i + 2; j < n; j++) { if (i === 0 && j === n - 1) continue; const c = points[j], d = points[(j + 1) % n]; if ((cross(a, b, c) * cross(a, b, d) < 0 && cross(c, d, a) * cross(c, d, b) < 0) || on(a, b, c) || on(a, b, d) || on(c, d, a) || on(c, d, b)) return false; } } return Math.abs(area) > eps; }; // ----------------------------------------------------------------------------- // Elev points layer (puntos de elevación + número) // ----------------------------------------------------------------------------- window.rt = window.rt || {}; window.rt.ensureElevLayer = function () { const map = window.__rtLeafletMap; if (!map || !window.L) return false; if (!window.rt._elevLayer) { window.rt._elevLayer = window.L.layerGroup().addTo(map); } if (!window.rt._elevGroups) window.rt._elevGroups = {}; return true; }; window.rt.clearElevPoints = function (id) { if (!window.rt.ensureElevLayer()) return; if (id) { const g = window.rt._elevGroups && window.rt._elevGroups[id]; if (g) { window.rt._elevLayer.removeLayer(g); delete window.rt._elevGroups[id]; } return; } // sin id => limpiar todo window.rt._elevLayer.clearLayers(); window.rt._elevGroups = {}; }; // points: [{ lat, lon, label }] // options: { radius, opacity, fillOpacity, showLabels, labelEvery } window.rt.drawElevPoints = function (id, points, options) { if (!window.rt.ensureElevLayer()) return; // borrar lo anterior con ese id if (window.rt._elevGroups[id]) { window.rt._elevLayer.removeLayer(window.rt._elevGroups[id]); delete window.rt._elevGroups[id]; } const radius = (options && options.radius) || 3; const opacity = (options && options.opacity) || 0.9; const fillOpacity = (options && options.fillOpacity) || 0.9; const showLabels = options && options.showLabels !== false; // default true const every = (options && options.labelEvery) || 1; // default 1 const showSlopeTooltips = !!(options && options.showSlopeTooltips); const baseClassName = (options && options.className) || ""; const defaultColor = (options && options.color) || "#facc15"; const group = window.L.layerGroup(); if (points && points.length) { for (let i = 0; i < points.length; i++) { const p = points[i]; const pointColor = (p && p.color) || defaultColor; const category = (p && p.category) ? String(p.category) : ""; const categoryClass = category ? `${baseClassName}--${category}` : ""; const className = [baseClassName, categoryClass, p && p.className].filter(Boolean).join(" "); const m = window.L.circleMarker([p.lat, p.lon], { radius: radius, weight: 1, opacity: opacity, fillOpacity: fillOpacity, color: pointColor, fillColor: pointColor, className: className, bubblingMouseEvents: false }); if (showLabels && every > 0 && (i % every === 0)) { const txt = (p.label != null ? String(p.label) : String(i)); // ------------------------------------------------------- // ✅ Evitar que el número pise la ruta: // si el tramo local es "vertical" -> etiqueta a un lado // si es "horizontal" -> arriba // ------------------------------------------------------- let dir = "top"; let off = [0, -3]; const prev = (i > 0) ? points[i - 1] : null; const next = (i < points.length - 1) ? points[i + 1] : null; const a = prev || p; const b = next || p; const dLat = Math.abs((b.lat) - (a.lat)); const dLon = Math.abs((b.lon) - (a.lon)); // tramo más vertical => poner a la derecha if (dLat > dLon) { dir = "right"; // puedes cambiar a "left" si prefieres off = [5, 0]; // separación lateral } m.bindTooltip(txt, { permanent: true, direction: dir, offset: off, opacity: 0.95, className: "rt-elev-label" }); } else if (showSlopeTooltips && p && typeof p.slope === "number" && Number.isFinite(p.slope)) { const label = (p.slope > 0 ? "+" : "") + p.slope.toFixed(1) + "%"; m.bindTooltip(label, { sticky: true, direction: "top", opacity: 0.92, className: "rt-elev-tooltip" }); } m.on("click", function (e) { if (window.rt.handleInspectorLayerClick(e)) return; if (!showSlopeTooltips || !p || typeof p.slope !== "number" || !Number.isFinite(p.slope)) return; if (!window.rt.isMobileLayout || !window.rt.isMobileLayout()) return; try { m.openTooltip(e && e.latlng); } catch { } }); m.addTo(group); } } group.addTo(window.rt._elevLayer); window.rt._elevGroups[id] = group; }; window.rt = window.rt || {}; window.rt = window.rt || {}; window.rt.addZoomIndicator = function () { const map = window.__rtLeafletMap; if (!map || !window.L) return; // evitar duplicados if (window.__rtZoomIndicator) return; const zoomContainer = map.zoomControl && map.zoomControl._container; if (!zoomContainer) return; // botones nativos const btnIn = zoomContainer.querySelector(".leaflet-control-zoom-in"); const btnOut = zoomContainer.querySelector(".leaflet-control-zoom-out"); if (!btnIn || !btnOut) return; // indicador const div = window.L.DomUtil.create("div", "leaflet-control-zoom-indicator-inset"); div.innerText = map.getZoom(); // que no robe clicks window.L.DomEvent.disableClickPropagation(div); window.L.DomEvent.disableScrollPropagation(div); // ✅ insertarlo ENTRE + y - zoomContainer.insertBefore(div, btnOut); map.on("zoomend", () => { div.innerText = map.getZoom(); }); window.__rtZoomIndicator = div; }; window.rt = window.rt || {}; window.rt.blockLeafletDoubleClick = function () { if (window.__rtDblClickBlocked) return; // evitar duplicados window.__rtDblClickBlocked = true; document.addEventListener("dblclick", function (e) { // si el doble click ocurre dentro de un mapa Leaflet, lo anulamos const isOnMap = e.target && e.target.closest && e.target.closest(".leaflet-container"); if (!isOnMap) return; e.preventDefault(); e.stopPropagation(); // clave: corta el evento incluso para listeners “antes” if (e.stopImmediatePropagation) e.stopImmediatePropagation(); return false; }, true); // CAPTURING: lo pillamos antes que Leaflet }; window.rt.isMobileLayout = function () { return !!(window.matchMedia && window.matchMedia("(max-width: 780px)").matches); }; window.rt.initMobileMapControls = function () { if (window.rt._mobileMapControlsReady) return; window.rt._mobileMapControlsReady = true; const holdMilliseconds = 620; const movementTolerance = 12; let holdTimer = null; let press = null; let suppressClickUntil = 0; const cancelHold = function () { if (holdTimer !== null) { window.clearTimeout(holdTimer); holdTimer = null; } press = null; }; document.addEventListener("pointerdown", function (event) { if (!window.rt.isMobileLayout()) return; if (event.isPrimary === false) { cancelHold(); return; } const mapContainer = event.target && event.target.closest ? event.target.closest(".leaflet-container") : null; if (!mapContainer) return; if (event.target.closest(".leaflet-control, .leaflet-marker-icon, .leaflet-popup")) return; cancelHold(); press = { pointerId: event.pointerId, clientX: event.clientX, clientY: event.clientY, mapContainer }; holdTimer = window.setTimeout(function () { const currentPress = press; holdTimer = null; if (!currentPress || !currentPress.mapContainer.isConnected) return; const map = window.__rtLeafletMap; if (!map || !window.L || !window.rt._dotNetHelper) return; const rect = currentPress.mapContainer.getBoundingClientRect(); const point = window.L.point( currentPress.clientX - rect.left, currentPress.clientY - rect.top); const latLng = map.containerPointToLatLng(point); suppressClickUntil = Date.now() + 900; try { navigator.vibrate?.(18); } catch { } window.rt._dotNetHelper.invokeMethodAsync("OnMobileMapLongPress", latLng.lat, latLng.lng); press = null; }, holdMilliseconds); }, true); document.addEventListener("pointermove", function (event) { if (!press || event.pointerId !== press.pointerId) return; if (Math.abs(event.clientX - press.clientX) > movementTolerance || Math.abs(event.clientY - press.clientY) > movementTolerance) { cancelHold(); } }, true); document.addEventListener("pointerup", cancelHold, true); document.addEventListener("pointercancel", cancelHold, true); document.addEventListener("click", function (event) { if (Date.now() >= suppressClickUntil) return; const isOnMap = event.target && event.target.closest ? event.target.closest(".leaflet-container") : null; if (!isOnMap) return; event.preventDefault(); event.stopPropagation(); if (event.stopImmediatePropagation) event.stopImmediatePropagation(); }, true); document.addEventListener("contextmenu", function (event) { if (!window.rt.isMobileLayout()) return; const isOnMap = event.target && event.target.closest ? event.target.closest(".leaflet-container") : null; if (isOnMap) event.preventDefault(); }, true); }; // ----------------------------------------------------------------------------- // Preview layer (línea completa) - NO toca la ruta actual // ----------------------------------------------------------------------------- window.rt = window.rt || {}; window.rt.ensurePreviewLayer = function () { const map = window.__rtLeafletMap; if (!map || !window.L) return false; if (!window.rt._previewLayer) { window.rt._previewLayer = window.L.layerGroup().addTo(map); } if (!window.rt._previewLines) window.rt._previewLines = {}; return true; }; window.rt.clearPreview = function () { if (!window.rt.ensurePreviewLayer()) return; window.rt._previewLayer.clearLayers(); window.rt._previewLines = {}; // ✅ también limpiar paradas destacadas if (window.rt._itStopsLayer) { window.rt._itStopsLayer.clearLayers(); window.rt._itStopGroups = {}; } }; window.rt.drawPreviewLine = function (id, coords, options) { if (!window.rt.ensurePreviewLayer()) return; const empty = !coords || coords.length === 0; const arrowId = id + "__arrow"; // toggle: si ya existe, lo quitamos (línea + flecha(s)) if (window.rt._previewLines[id]) { window.rt._previewLayer.removeLayer(window.rt._previewLines[id]); delete window.rt._previewLines[id]; if (window.rt._previewLines[arrowId]) { window.rt._previewLayer.removeLayer(window.rt._previewLines[arrowId]); delete window.rt._previewLines[arrowId]; } return; } if (empty) return; const map = window.__rtLeafletMap; const color = (options && options.color) || "#60a5fa"; const weight = (options && options.weight) || 4; const opacity = (options && options.opacity) || 0.6; const dashArray = options && options.dashArray; const arrowEnd = !!(options && options.arrowEnd); const arrowClass = dashArray ? "rt-arrow rt-arrow--dashed" : "rt-arrow"; const arrowInnerHtml = dashArray ? '' : ''; const lineOpts = { color, weight, opacity }; if (dashArray) lineOpts.dashArray = dashArray; const line = window.L.polyline(coords, lineOpts); // ✅ Hacer la línea clicable y avisar a Blazor line.on("click", function (e) { try { if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(e)) return; if (e && e.originalEvent) { if (e.originalEvent.preventDefault) e.originalEvent.preventDefault(); if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation(); if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation(); } if (window.rt._dotNetHelper) { window.rt._dotNetHelper.invokeMethodAsync("OnPreviewItinerarioClick", id); } } catch (e) { } }); line.addTo(window.rt._previewLayer); window.rt._previewLines[id] = line; // ✅ Flecha al FINAL if (arrowEnd && coords.length >= 2 && map) { // pane por encima de las paradas if (!map.getPane("rtArrowPane")) { const pane = map.createPane("rtArrowPane"); pane.style.zIndex = 760; // > rtStopsPane (750) pane.style.pointerEvents = "none"; // no roba clicks } const size = Math.max(14, Math.min(28, weight * 4)); function addArrow(from, to, storeId) { const lat1 = from[0] * Math.PI / 180, lon1 = from[1] * Math.PI / 180; const lat2 = to[0] * Math.PI / 180, lon2 = to[1] * Math.PI / 180; const y = Math.sin(lon2 - lon1) * Math.cos(lat2); const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); let brng = Math.atan2(y, x) * 180 / Math.PI; brng = (brng + 360) % 360; const icon = window.L.divIcon({ className: "rt-arrow-icon", html: `
${arrowInnerHtml}
`, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); const marker = window.L.marker(to, { icon, pane: "rtArrowPane", interactive: false, zIndexOffset: 1000 }); marker.addTo(window.rt._previewLayer); window.rt._previewLines[storeId] = marker; } addArrow(coords[coords.length - 2], coords[coords.length - 1], arrowId); } }; window.rt = window.rt || {}; window.rt.getZoom = function () { const map = window.__rtLeafletMap; return map ? map.getZoom() : 14; }; window.rt = window.rt || {}; // Devuelve true si el click está dentro del círculo (en píxeles) window.rt.isClickInsideCirclePx = function (clickLat, clickLon, targetLat, targetLon, radiusPx) { const map = window.__rtLeafletMap; if (!map || !window.L) return false; const pClick = map.latLngToContainerPoint([clickLat, clickLon]); const pTarget = map.latLngToContainerPoint([targetLat, targetLon]); const dx = pClick.x - pTarget.x; const dy = pClick.y - pTarget.y; return (dx * dx + dy * dy) <= (radiusPx * radiusPx); }; // ----------------------------------------------------------------------------- // Itinerario stops layer (paradas destacadas) - toggle por id // ----------------------------------------------------------------------------- window.rt = window.rt || {}; window.rt.ensureItStopsLayer = function () { const map = window.__rtLeafletMap; if (!map || !window.L) return false; // pane por encima para que queden SOBRE las líneas if (!map.getPane("rtStopsPane")) { const pane = map.createPane("rtStopsPane"); pane.style.zIndex = 750; } if (!window.rt._itStopsLayer) { window.rt._itStopsLayer = window.L.layerGroup().addTo(map); } if (!window.rt._itStopGroups) window.rt._itStopGroups = {}; return true; }; window.rt.createHighlightedStopMarker = function (stop, pane, options) { if (!stop || !window.L) return null; const lat = Number(stop.lat); const lon = Number(stop.lon); if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; const toPositiveNumber = function (value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; }; const toOpacity = function (value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) ? Math.min(1, Math.max(0, parsed)) : fallback; }; const radius = toPositiveNumber(options && options.radius, 7); const weight = toPositiveNumber(options && options.weight, 2); const color = String((options && options.color) || "#0f172a"); const fillColor = String((options && options.fillColor) || "#22c55e"); const opacity = toOpacity(options && options.opacity, 1); const fillOpacity = toOpacity(options && options.fillOpacity, 1); const size = Math.max(14, Math.ceil((radius * 2) + weight)); const markerElement = document.createElement("span"); markerElement.className = "rt-highlighted-stop-marker"; const disc = document.createElement("span"); disc.className = "rt-highlighted-stop-marker__disc"; disc.style.inset = `${weight}px`; disc.style.backgroundColor = fillColor; disc.style.opacity = String(fillOpacity); markerElement.appendChild(disc); const ring = document.createElement("span"); ring.className = "rt-highlighted-stop-marker__ring"; ring.style.borderColor = color; ring.style.borderWidth = `${weight}px`; ring.style.opacity = String(opacity); markerElement.appendChild(ring); const rawBearing = stop.bearing; const bearing = rawBearing === null || rawBearing === undefined || rawBearing === "" ? null : Number(rawBearing); if (Number.isFinite(bearing)) { const arrow = document.createElement("span"); arrow.className = "rt-stop-direction-arrow"; arrow.style.setProperty("--stop-bearing", `${bearing}deg`); markerElement.appendChild(arrow); } const icon = window.L.divIcon({ className: "rt-highlighted-stop-icon", html: markerElement, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); const marker = window.L.marker([lat, lon], { pane, icon, interactive: true, keyboard: false, bubblingMouseEvents: false, zIndexOffset: 1000 }); if (stop.code && window.rt._dotNetHelper) { marker.on("click", function (event) { try { if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(event)) return; if (event && event.originalEvent) { if (event.originalEvent.preventDefault) event.originalEvent.preventDefault(); if (event.originalEvent.stopPropagation) event.originalEvent.stopPropagation(); if (event.originalEvent.stopImmediatePropagation) event.originalEvent.stopImmediatePropagation(); } window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", stop.code); } catch (error) { } }); } return marker; }; // stops: [{lat, lon, code, bearing}] window.rt.toggleItStops = function (id, stops, options) { if (!window.rt.ensureItStopsLayer()) return; // toggle: si existe => quitar if (window.rt._itStopGroups[id]) { window.rt._itStopsLayer.removeLayer(window.rt._itStopGroups[id]); delete window.rt._itStopGroups[id]; return; } const radius = (options && options.radius) || 7; const color = (options && options.color) || "#0f172a"; const fillColor = (options && options.fillColor) || "#22c55e"; const weight = (options && options.weight) || 2; const opacity = (options && options.opacity) || 1.0; const fillOpacity = (options && options.fillOpacity) || 1.0; const group = window.L.layerGroup(); if (stops && stops.length) { for (let i = 0; i < stops.length; i++) { const s = stops[i]; const marker = window.rt.createHighlightedStopMarker(s, "rtStopsPane", { radius, color, weight, opacity, fillColor, fillOpacity }); if (marker) marker.addTo(group); } } group.addTo(window.rt._itStopsLayer); window.rt._itStopGroups[id] = group; }; // ----------------------------------------------------------------------------- // Route solution stops layer (paradas destacadas de la solución seleccionada) // ----------------------------------------------------------------------------- window.rt = window.rt || {}; window.rt.ensureRouteStopsLayer = function () { const map = window.__rtLeafletMap; if (!map || !window.L) return false; if (!map.getPane("rtRouteStopsPane")) { const pane = map.createPane("rtRouteStopsPane"); pane.style.zIndex = 752; pane.style.pointerEvents = "none"; } if (!window.rt._routeStopsLayer) { window.rt._routeStopsLayer = window.L.layerGroup().addTo(map); } if (!window.rt._routeStopGroups) window.rt._routeStopGroups = {}; return true; }; window.rt.drawRouteStops = function (id, stops, options) { if (!window.rt.ensureRouteStopsLayer()) return; if (window.rt._routeStopGroups[id]) { window.rt._routeStopsLayer.removeLayer(window.rt._routeStopGroups[id]); delete window.rt._routeStopGroups[id]; } if (!stops || !stops.length) return; const radius = (options && options.radius) || 8; const color = (options && options.color) || "#0f172a"; const fillColor = (options && options.fillColor) || "#22c55e"; const weight = (options && options.weight) || 2; const opacity = (options && options.opacity) || 1.0; const fillOpacity = (options && options.fillOpacity) || 1.0; const group = window.L.layerGroup(); for (let i = 0; i < stops.length; i++) { const s = stops[i]; const marker = window.rt.createHighlightedStopMarker(s, "rtRouteStopsPane", { radius, color, weight, opacity, fillColor, fillOpacity }); if (marker) marker.addTo(group); } group.addTo(window.rt._routeStopsLayer); window.rt._routeStopGroups[id] = group; }; window.rt.clearRouteStops = function (id) { if (!window.rt.ensureRouteStopsLayer()) return; if (id) { const group = window.rt._routeStopGroups && window.rt._routeStopGroups[id]; if (group) { window.rt._routeStopsLayer.removeLayer(group); delete window.rt._routeStopGroups[id]; } return; } window.rt._routeStopsLayer.clearLayers(); window.rt._routeStopGroups = {}; }; // ----------------------------------------------------------------------------- // Origin / destination draggable markers // ----------------------------------------------------------------------------- window.rt = window.rt || {}; window.rt.ensureEndpointLayer = function () { const map = window.__rtLeafletMap; if (!map || !window.L) return false; if (!map.getPane("rtEndpointPane")) { const pane = map.createPane("rtEndpointPane"); pane.style.zIndex = 780; } if (!window.rt._endpointLayer) { window.rt._endpointLayer = window.L.layerGroup().addTo(map); } if (!window.rt._endpointMarkers) window.rt._endpointMarkers = {}; return true; }; window.rt.setEndpointMarker = function (kind, lat, lon, options) { if (!window.rt.ensureEndpointLayer()) return; if (window.rt._endpointMarkers[kind]) { window.rt._endpointLayer.removeLayer(window.rt._endpointMarkers[kind]); delete window.rt._endpointMarkers[kind]; } if (typeof lat !== "number" || typeof lon !== "number") return; const color = (options && options.color) || "#22c55e"; const title = (options && options.title) || ""; const cssKind = kind === "destination" ? "destination" : "origin"; const requestedSize = (options && options.size) || 18; const size = window.rt.isMobileLayout && window.rt.isMobileLayout() ? Math.max(24, requestedSize) : requestedSize; const icon = window.L.divIcon({ className: "rt-endpoint-icon", html: `
`, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); const marker = window.L.marker([lat, lon], { pane: "rtEndpointPane", draggable: true, autoPan: true, icon, title }); const stopEvent = function (e) { if (!e || !e.originalEvent) return; if (e.originalEvent.preventDefault) e.originalEvent.preventDefault(); if (e.originalEvent.stopPropagation) e.originalEvent.stopPropagation(); if (e.originalEvent.stopImmediatePropagation) e.originalEvent.stopImmediatePropagation(); }; marker.on("click", function (event) { if (window.rt._exclusionEditMode) return; if (window.rt.handleInspectorLayerClick(event)) return; stopEvent(event); }); marker.on("mousedown", stopEvent); marker.on("dragend", function (e) { try { const pos = e.target.getLatLng(); if (window.rt._dotNetHelper) { window.rt._dotNetHelper.invokeMethodAsync("OnEndpointDragged", kind, pos.lat, pos.lng); } } catch (err) { } }); marker.addTo(window.rt._endpointLayer); window.rt._endpointMarkers[kind] = marker; }; window.rt.clearEndpointMarkers = function (kind) { if (!window.rt.ensureEndpointLayer()) return; if (kind) { const marker = window.rt._endpointMarkers && window.rt._endpointMarkers[kind]; if (marker) { window.rt._endpointLayer.removeLayer(marker); delete window.rt._endpointMarkers[kind]; } return; } window.rt._endpointLayer.clearLayers(); window.rt._endpointMarkers = {}; }; // ----------------------------------------------------------------------------- // Punto Z y lugares cercanos del callejero // ----------------------------------------------------------------------------- window.rt.ensureInspectorLayer = function () { const map = window.__rtLeafletMap; if (!map || !window.L) return false; if (!map.getPane("rtInspectorPane")) { const pane = map.createPane("rtInspectorPane"); pane.style.zIndex = 790; } const inspectorTooltipPane = map.getPane("rtInspectorTooltipPane") || map.createPane("rtInspectorTooltipPane"); inspectorTooltipPane.style.zIndex = 10000; inspectorTooltipPane.style.pointerEvents = "none"; if (!window.rt._inspectorLayer) { window.rt._inspectorLayer = window.L.layerGroup().addTo(map); } return true; }; window.rt.setInspectorMarkers = function (lat, lon, places) { if (!window.rt.ensureInspectorLayer()) return; window.rt._inspectorLayer.clearLayers(); const zLat = Number(lat); const zLon = Number(lon); if (!Number.isFinite(zLat) || !Number.isFinite(zLon)) return; const puntoZ = window.L.circleMarker([zLat, zLon], { pane: "rtInspectorPane", radius: 8, color: "#713f12", weight: 2, opacity: 1, fillColor: "#fde047", fillOpacity: 0.42, bubblingMouseEvents: false }); puntoZ.bindTooltip("Punto Z", { pane: "rtInspectorTooltipPane", direction: "top", offset: [0, -12], opacity: 0.95, className: "rt-inspector-tooltip" }); puntoZ.on("click", function (event) { if (window.rt._exclusionEditMode) return; window.rt.handleInspectorLayerClick(event); }); puntoZ.addTo(window.rt._inspectorLayer); const lugares = Array.isArray(places) ? places.slice(0, 3) : []; const coincidencias = {}; lugares.forEach(function (place) { const placeLat = Number(place && place.latitud); const placeLon = Number(place && place.longitud); if (!Number.isFinite(placeLat) || !Number.isFinite(placeLon)) return; const clave = `${placeLat.toFixed(6)}|${placeLon.toFixed(6)}`; coincidencias[clave] = (coincidencias[clave] || 0) + 1; }); const posicionCoincidente = {}; lugares.forEach(function (place, index) { const placeLat = Number(place && place.latitud); const placeLon = Number(place && place.longitud); if (!Number.isFinite(placeLat) || !Number.isFinite(placeLon)) return; const clave = `${placeLat.toFixed(6)}|${placeLon.toFixed(6)}`; const totalCoincidentes = coincidencias[clave] || 1; const posicion = posicionCoincidente[clave] || 0; posicionCoincidente[clave] = posicion + 1; let offsetX = 0; if (totalCoincidentes === 2) offsetX = posicion === 0 ? -14 : 14; else if (totalCoincidentes >= 3) offsetX = [-26, 0, 26][Math.min(posicion, 2)]; window.L.polyline( [[zLat, zLon], [placeLat, placeLon]], { pane: "rtInspectorPane", color: "#0f766e", weight: 1.5, opacity: 0.75, dashArray: "4 5", interactive: false }) .addTo(window.rt._inspectorLayer); const numero = index + 1; const size = 24; const icon = window.L.divIcon({ className: "rt-inspector-place-icon", html: `${numero}`, iconSize: [size, size], iconAnchor: [(size / 2) - offsetX, size / 2] }); const texto = place && typeof place.texto === "string" ? place.texto : `Lugar ${numero}`; const marker = window.L.marker([placeLat, placeLon], { pane: "rtInspectorPane", icon, title: texto, keyboard: false, bubblingMouseEvents: false, zIndexOffset: 20 + numero }); const tooltip = document.createElement("span"); tooltip.textContent = texto; marker.bindTooltip(tooltip, { pane: "rtInspectorTooltipPane", direction: "top", offset: [offsetX, -24], opacity: 0.95, className: "rt-inspector-tooltip" }); marker.on("click", function (event) { if (window.rt._exclusionEditMode) return; window.rt.handleInspectorLayerClick(event); }); marker.addTo(window.rt._inspectorLayer); }); }; window.rt.clearInspectorMarkers = function () { if (window.rt._inspectorLayer) { window.rt._inspectorLayer.clearLayers(); } }; window.rt.setSelectedStopMarker = function (lat, lon) { const map = window.__rtLeafletMap; if (!map || !window.L) return; if (!map.getPane("rtSelectedStopPane")) { const pane = map.createPane("rtSelectedStopPane"); pane.style.zIndex = 785; pane.style.pointerEvents = "none"; } window.rt.clearSelectedStopMarker(); const stopLat = Number(lat); const stopLon = Number(lon); if (!Number.isFinite(stopLat) || !Number.isFinite(stopLon)) return; const icon = window.L.divIcon({ className: "rt-selected-stop-icon", html: '', iconSize: [28, 28], iconAnchor: [14, 14] }); window.rt._selectedStopMarker = window.L.marker([stopLat, stopLon], { pane: "rtSelectedStopPane", icon, interactive: false, keyboard: false }).addTo(map); }; window.rt.clearSelectedStopMarker = function () { const map = window.__rtLeafletMap; if (map && window.rt._selectedStopMarker) { map.removeLayer(window.rt._selectedStopMarker); } window.rt._selectedStopMarker = null; }; window.rt.setInspectorWalk = function (points, visible) { window.rt._inspectorWalkLayer?.remove(); window.rt._inspectorWalkLayer = null; const map = window.__rtLeafletMap; if (!visible || !map || !window.L || points.length < 2) return; window.rt._inspectorWalkLayer = window.L.polyline(points, { color: "#0891b2", weight: 5, dashArray: "2 7", lineCap: "round", interactive: false }).addTo(map); if (!points.some(p => map.getBounds().contains(p))) map.panTo(points[0]); }; window.rt = window.rt || {}; window.rt.scrollToHorarioProximo = function () { const el = document.getElementById("horario-proximo-anchor"); if (!el) return; el.scrollIntoView({ block: "center", behavior: "auto" }); }; window.rt.initDraggablePopups = function () { if (window.rt._draggablePopupsReady) return; window.rt._draggablePopupsReady = true; window.rt._draggablePopupPositions = window.rt._draggablePopupPositions || {}; window.rt._draggablePopupZCounter = Number.isFinite(window.rt._draggablePopupZCounter) ? window.rt._draggablePopupZCounter : 1200; const padding = 8; const edgePeekX = 28; const edgePeekY = 42; function dragKey(popup) { return popup && (popup.dataset.dragKey || popup.id || ""); } function clamp(value, min, max) { if (max < min) return min; return Math.min(Math.max(value, min), max); } function parseZ(value) { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : null; } function ensurePopupZIndex(popup) { if (!popup) return; const inlineZ = parseZ(popup.style.zIndex); if (inlineZ != null) { window.rt._draggablePopupZCounter = Math.max(window.rt._draggablePopupZCounter, inlineZ); return; } const computedZ = parseZ(window.getComputedStyle(popup).zIndex); if (computedZ != null) { window.rt._draggablePopupZCounter = Math.max(window.rt._draggablePopupZCounter, computedZ); } window.rt._draggablePopupZCounter += 1; popup.style.zIndex = String(window.rt._draggablePopupZCounter); } function bringPopupToFront(popup) { if (!popup) return; ensurePopupZIndex(popup); window.rt._draggablePopupZCounter += 1; popup.style.zIndex = String(window.rt._draggablePopupZCounter); } function clampPosition(left, top, width, height) { return { left: clamp(left, edgePeekX - width, window.innerWidth - edgePeekX), top: clamp(top, edgePeekY - height, window.innerHeight - edgePeekY) }; } function constrainFreeHeight(popup, height) { if (!popup || !popup.classList.contains("route-panel-overlay")) return null; const maxHeight = Math.max(260, window.innerHeight - (padding * 2)); return Math.min(height || maxHeight, maxHeight); } function isHandleRecoverablyHidden(popup) { if (!popup) return false; const handle = popup.querySelector(".draggable-popup-handle"); if (!handle) return false; const rect = handle.getBoundingClientRect(); const visibleTop = Math.max(rect.top, 0); const visibleBottom = Math.min(rect.bottom, window.innerHeight); const visibleHeight = Math.max(0, visibleBottom - visibleTop); return visibleHeight < 18; } function setFreePosition(popup, left, top, width, height) { popup.classList.add("draggable-popup--free"); popup.style.setProperty("--drag-left", `${left}px`); popup.style.setProperty("--drag-top", `${top}px`); popup.style.right = "auto"; popup.style.bottom = "auto"; popup.style.width = `${width}px`; const constrainedHeight = constrainFreeHeight(popup, height); if (constrainedHeight != null) { popup.style.height = `${constrainedHeight}px`; popup.style.maxHeight = `${constrainedHeight}px`; } else { popup.style.removeProperty("height"); popup.style.removeProperty("max-height"); } } function savePosition(popup) { const key = dragKey(popup); if (!key) return; const rect = popup.getBoundingClientRect(); window.rt._draggablePopupPositions[key] = { left: rect.left, top: rect.top, width: rect.width, height: rect.height }; } function applySavedPositions() { document.querySelectorAll(".draggable-popup").forEach((popup) => { const key = dragKey(popup); const saved = key ? window.rt._draggablePopupPositions[key] : null; if (saved && !popup.classList.contains("draggable-popup--free")) { const rect = popup.getBoundingClientRect(); const width = saved.width || rect.width; const height = saved.height || rect.height; const pos = clampPosition(saved.left, saved.top, width, height); setFreePosition(popup, pos.left, pos.top, width, height); } ensurePopupZIndex(popup); }); } document.addEventListener("pointerdown", function (event) { const popup = event.target && event.target.closest ? event.target.closest(".draggable-popup") : null; if (!popup) return; bringPopupToFront(popup); if (window.rt.isMobileLayout && window.rt.isMobileLayout()) return; if (event.button !== 0) return; if (event.target.closest("button,a,input,textarea,select,label")) return; const handle = event.target && event.target.closest ? event.target.closest(".draggable-popup-handle") : null; const fallbackWholePopup = !handle && popup.classList.contains("draggable-popup--free") && isHandleRecoverablyHidden(popup); if (!handle && !fallbackWholePopup) return; const dragSurface = handle || popup; event.preventDefault(); const rect = popup.getBoundingClientRect(); const width = rect.width; const height = rect.height; const startX = event.clientX; const startY = event.clientY; const startLeft = rect.left; const startTop = rect.top; const initial = clampPosition(startLeft, startTop, width, height); setFreePosition(popup, initial.left, initial.top, width, height); popup.classList.add("draggable-popup--dragging"); try { dragSurface.setPointerCapture(event.pointerId); } catch { } function onMove(moveEvent) { const pos = clampPosition( startLeft + moveEvent.clientX - startX, startTop + moveEvent.clientY - startY, width, height); popup.style.setProperty("--drag-left", `${pos.left}px`); popup.style.setProperty("--drag-top", `${pos.top}px`); } function onUp(upEvent) { document.removeEventListener("pointermove", onMove, true); document.removeEventListener("pointerup", onUp, true); document.removeEventListener("pointercancel", onUp, true); popup.classList.remove("draggable-popup--dragging"); savePosition(popup); try { dragSurface.releasePointerCapture(upEvent.pointerId); } catch { } } document.addEventListener("pointermove", onMove, true); document.addEventListener("pointerup", onUp, true); document.addEventListener("pointercancel", onUp, true); }, true); document.addEventListener("focusin", function (event) { const popup = event.target && event.target.closest ? event.target.closest(".draggable-popup") : null; if (!popup) return; bringPopupToFront(popup); }, true); window.addEventListener("resize", function () { document.querySelectorAll(".draggable-popup--free").forEach((popup) => { const rect = popup.getBoundingClientRect(); const pos = clampPosition(rect.left, rect.top, rect.width, rect.height); setFreePosition(popup, pos.left, pos.top, rect.width, rect.height); savePosition(popup); }); }); const observer = new MutationObserver(applySavedPositions); observer.observe(document.body, { childList: true, subtree: true }); applySavedPositions(); }; window.rt.saveLocalValueResult = function (key, value) { try { const storage = window.localStorage; if (!storage) { return { ok: false, errorName: "StorageUnavailable", errorMessage: "El navegador no expone localStorage." }; } storage.setItem(key, value ?? ""); return { ok: true }; } catch (error) { return { ok: false, errorName: error?.name ? String(error.name) : "StorageError", errorMessage: error?.message ? String(error.message) : String(error ?? "Error desconocido al guardar en localStorage.") }; } }; window.rt.loadLocalValueResult = function (key, maxItems, maxBytes, recoveryKey) { let originalItemCount = null; let itemCount = null; let originalByteLength = null; let byteLength = null; let trimmed = false; let backupSaved = false; try { const storage = window.localStorage; if (!storage) { return { ok: false, found: false, value: null, errorName: "StorageUnavailable", errorMessage: "El navegador no expone localStorage." }; } const originalValue = storage.getItem(key); let value = originalValue; const normalizedMaxItems = Number.isInteger(maxItems) && maxItems > 0 ? maxItems : null; const normalizedMaxBytes = Number.isInteger(maxBytes) && maxBytes > 0 ? maxBytes : null; originalByteLength = originalValue === null ? 0 : new Blob([originalValue]).size; byteLength = originalByteLength; if (value !== null && value.trim() !== "" && (normalizedMaxItems !== null || normalizedMaxBytes !== null)) { let parsedValue = null; try { parsedValue = JSON.parse(value); } catch { // El servidor conserva su recuperacion detallada para JSON danado. } if (Array.isArray(parsedValue)) { originalItemCount = parsedValue.length; const items = normalizedMaxItems === null ? parsedValue.slice() : parsedValue.slice(0, normalizedMaxItems); value = JSON.stringify(items); byteLength = new Blob([value]).size; while (normalizedMaxBytes !== null && items.length > 0 && byteLength > normalizedMaxBytes) { items.pop(); value = JSON.stringify(items); byteLength = new Blob([value]).size; } itemCount = items.length; trimmed = itemCount < originalItemCount || (normalizedMaxBytes !== null && originalByteLength > normalizedMaxBytes); if (trimmed) { if (typeof recoveryKey !== "string" || recoveryKey.trim() === "") { throw new Error("No se ha indicado una clave para respaldar el historial antes de recortarlo."); } storage.setItem(recoveryKey, originalValue); backupSaved = true; storage.setItem(key, value); } else { value = originalValue; byteLength = originalByteLength; } } } return { ok: true, found: value !== null, value, trimmed, backupSaved, originalItemCount, itemCount, originalByteLength, byteLength }; } catch (error) { return { ok: false, found: false, value: null, errorName: error?.name ? String(error.name) : "StorageError", errorMessage: error?.message ? String(error.message) : String(error ?? "Error desconocido al leer localStorage."), trimmed, backupSaved, originalItemCount, itemCount, originalByteLength, byteLength }; } }; window.rt.saveLocalValue = function (key, value) { window.rt.saveLocalValueResult(key, value); }; window.rt.loadLocalValue = function (key) { const result = window.rt.loadLocalValueResult(key); return result.ok ? result.value : null; }; window.rt.clearLocalBrowserData = async function () { const deleteIndexedDb = function (name) { return new Promise((resolve) => { try { const req = window.indexedDB.deleteDatabase(name); req.onsuccess = resolve; req.onerror = resolve; req.onblocked = resolve; } catch { resolve(); } }); }; try { window.localStorage?.clear(); } catch { } try { window.sessionStorage?.clear(); } catch { } try { if (window.caches?.keys) { const keys = await window.caches.keys(); await Promise.all(keys.map((key) => window.caches.delete(key))); } } catch { } try { if (navigator.serviceWorker?.getRegistrations) { const registrations = await navigator.serviceWorker.getRegistrations(); await Promise.all(registrations.map((registration) => registration.unregister())); } } catch { } try { if (window.indexedDB?.databases) { const databases = await window.indexedDB.databases(); await Promise.all((databases || []) .filter((database) => database && database.name) .map((database) => deleteIndexedDb(database.name))); } } catch { } }; window.rt.reloadPage = function () { window.location.reload(); };