Files
RutasDBUS/RutasDBUS/wwwroot/app.js
Pedro 9f312d2ca9 v-20260914a
- punto z mas pequeño
- parada destacada
- restaurar valores por itinerairos y lineas
- intercambio en el buscar
- etiqyetas de soluciones elimiandas
- aumentado el nuemro de puntos de exclusion
- arreglado zoans de exlusion sin solucion
2026-09-14 14:25:28 +02:00

2218 lines
76 KiB
JavaScript

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("<br>");
};
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 ? '<span class="rt-arrow-dash-head"></span>' : '';
const icon = window.L.divIcon({
className: "rt-arrow-icon",
html: `<div class="${arrowClass}" style="
width:${size}px;height:${size}px;
transform: rotate(${brng}deg);
--arrow-color:${arrowColor};
">${arrowInnerHtml}</div>`,
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 ? '<span class="rt-arrow-dash-head"></span>' : '';
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: `<div class="${arrowClass}" style="
width:${size}px;height:${size}px;
transform: rotate(${brng}deg);
--arrow-color:${color};
">${arrowInnerHtml}</div>`,
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: `<div class="rt-endpoint-marker rt-endpoint-marker--${cssKind}" style="--endpoint-color:${color}; width:${size}px; height:${size}px;"></div>`,
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: `<span class="rt-inspector-place-marker">${numero}</span>`,
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: '<span class="rt-selected-stop-marker"></span>',
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();
};