1593 lines
53 KiB
JavaScript
1593 lines
53 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) {
|
|
document.addEventListener("keydown", function (e) {
|
|
if (e.key === "Z" || e.key === "z") dotNetHelper.invokeMethodAsync("SetInspectorActivo", true);
|
|
}, true);
|
|
|
|
document.addEventListener("keyup", function (e) {
|
|
if (e.key === "Z" || e.key === "z") dotNetHelper.invokeMethodAsync("SetInspectorActivo", false);
|
|
}, true);
|
|
|
|
window.addEventListener("blur", function () {
|
|
dotNetHelper.invokeMethodAsync("SetInspectorActivo", false);
|
|
});
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// 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.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.clearRoutes = function () {
|
|
if (!window.rt.ensureRouteLayer()) return;
|
|
window.rt._routeLayer.clearLayers();
|
|
window.rt._routeLines = {};
|
|
window.rt._routeCoords = {};
|
|
|
|
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._routeLines[id]) {
|
|
window.rt._routeLayer.removeLayer(window.rt._routeLines[id]);
|
|
delete window.rt._routeLines[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 elevationSegments = Array.isArray(options && options.elevationSegments)
|
|
? options.elevationSegments
|
|
: [];
|
|
|
|
const attachRouteClick = function (line) {
|
|
line.on("click", function (e) {
|
|
try {
|
|
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();
|
|
|
|
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"
|
|
: "-";
|
|
|
|
return [
|
|
"Pendiente: " + slopeText,
|
|
"Dist. anterior: " + distanceText,
|
|
"Altura: " + elevationText
|
|
].join("<br>");
|
|
};
|
|
|
|
elevationSegments.forEach(function (segment) {
|
|
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",
|
|
bubblingMouseEvents: false
|
|
};
|
|
if (dashArray) segmentOpts.dashArray = dashArray;
|
|
|
|
const segmentLine = buildLine(segmentCoords, segmentOpts);
|
|
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.setTooltipContent(elevationTooltip(segment, progress));
|
|
};
|
|
segmentLine.on("mouseover", updateTooltipAtCursor);
|
|
segmentLine.on("mousemove", updateTooltipAtCursor);
|
|
segmentLine.on("click", function (e) {
|
|
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);
|
|
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) {
|
|
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;
|
|
|
|
map.panTo(geometryBounds.getCenter(), {
|
|
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;
|
|
};
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// 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.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 (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.addStopDirectionMarker = function (group, stop, pane, radius) {
|
|
if (!group || !stop || !window.L) return;
|
|
|
|
const bearing = Number(stop.bearing);
|
|
const lat = Number(stop.lat);
|
|
const lon = Number(stop.lon);
|
|
if (!Number.isFinite(bearing) || !Number.isFinite(lat) || !Number.isFinite(lon)) return;
|
|
|
|
const size = Math.max(14, Number(radius || 7) * 2);
|
|
const icon = window.L.divIcon({
|
|
className: "rt-stop-direction-icon",
|
|
html: `<span class="rt-stop-direction-arrow" style="--stop-bearing:${bearing}deg"></span>`,
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2]
|
|
});
|
|
|
|
window.L.marker([lat, lon], {
|
|
pane,
|
|
icon,
|
|
interactive: false,
|
|
keyboard: false,
|
|
zIndexOffset: 1000
|
|
}).addTo(group);
|
|
};
|
|
|
|
// 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 m = window.L.circleMarker([s.lat, s.lon], {
|
|
pane: "rtStopsPane",
|
|
radius,
|
|
color,
|
|
weight,
|
|
opacity,
|
|
fillColor,
|
|
fillOpacity,
|
|
interactive: true
|
|
});
|
|
|
|
if (s.code && window.rt._dotNetHelper) {
|
|
m.on("click", function (e) {
|
|
try {
|
|
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();
|
|
}
|
|
|
|
window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", s.code);
|
|
} catch (err) { }
|
|
});
|
|
}
|
|
|
|
m.addTo(group);
|
|
window.rt.addStopDirectionMarker(group, s, "rtStopsPane", radius);
|
|
}
|
|
}
|
|
|
|
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 m = window.L.circleMarker([s.lat, s.lon], {
|
|
pane: "rtRouteStopsPane",
|
|
radius,
|
|
color,
|
|
weight,
|
|
opacity,
|
|
fillColor,
|
|
fillOpacity,
|
|
interactive: true
|
|
});
|
|
|
|
if (s.code && window.rt._dotNetHelper) {
|
|
m.on("click", function (e) {
|
|
try {
|
|
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();
|
|
}
|
|
|
|
window.rt._dotNetHelper.invokeMethodAsync("OnHighlightedStopClick", s.code);
|
|
} catch (err) { }
|
|
});
|
|
}
|
|
|
|
m.addTo(group);
|
|
window.rt.addStopDirectionMarker(group, s, "rtRouteStopsPane", radius);
|
|
}
|
|
|
|
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", stopEvent);
|
|
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: 12,
|
|
color: "#713f12",
|
|
weight: 3,
|
|
opacity: 1,
|
|
fillColor: "#fde047",
|
|
fillOpacity: 0.42,
|
|
bubblingMouseEvents: false
|
|
});
|
|
puntoZ.bindTooltip("Punto Z", {
|
|
pane: "rtInspectorTooltipPane",
|
|
direction: "top",
|
|
offset: [0, -16],
|
|
opacity: 0.95,
|
|
className: "rt-inspector-tooltip"
|
|
});
|
|
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.addTo(window.rt._inspectorLayer);
|
|
});
|
|
};
|
|
|
|
window.rt.clearInspectorMarkers = function () {
|
|
if (window.rt._inspectorLayer) {
|
|
window.rt._inspectorLayer.clearLayers();
|
|
}
|
|
};
|
|
|
|
|
|
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.saveLocalValue = function (key, value) {
|
|
try {
|
|
if (!window.localStorage) return;
|
|
window.localStorage.setItem(key, value ?? "");
|
|
} catch { }
|
|
};
|
|
|
|
window.rt.loadLocalValue = function (key) {
|
|
try {
|
|
if (!window.localStorage) return null;
|
|
return window.localStorage.getItem(key);
|
|
} catch {
|
|
return 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();
|
|
};
|