alternativasBus,
- ParametrosPlanificadorRuta parametros)
- {
- var duraciones = alternativasBus
- .Select(DuracionVisibleOrdenacion)
- .Where(double.IsFinite)
- .ToList();
-
- var costesAndar = alternativasBus
- .Select(x => CalcularCosteAndarEquilibradoSegundos(x, parametros))
- .Where(double.IsFinite)
- .ToList();
-
- if (duraciones.Count == 0 || costesAndar.Count == 0)
- return (0, 0);
-
- var sacrificioTiempo = CalcularSacrificioNormalizado(
- DuracionVisibleOrdenacion(alternativa),
- duraciones.Min(),
- duraciones.Max());
-
- var sacrificioAndar = CalcularSacrificioNormalizado(
- CalcularCosteAndarEquilibradoSegundos(alternativa, parametros),
- costesAndar.Min(),
- costesAndar.Max());
-
- return (sacrificioTiempo, sacrificioAndar);
+ var score = distanciaBus + (distanciaAndando * factor);
+ return double.IsFinite(score) && score >= 0
+ ? score
+ : double.PositiveInfinity;
}
private static double CalcularCosteAndarEquilibradoSegundos(
AlternativaRuta alternativa,
ParametrosPlanificadorRuta parametros)
{
- var distanciaCorregida =
- UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieInicioAprox, parametros) +
- UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieTransbordosAprox, parametros) +
- UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
+ var distanciaCorregida = CalcularDistanciaAndandoCorregidaMetros(alternativa, parametros);
if (!double.IsFinite(distanciaCorregida) || distanciaCorregida < 0)
return double.PositiveInfinity;
@@ -2921,18 +2975,12 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
return UtilidadesTransitoDbus.EstimarDuracionPieSegundos(distanciaCorregida, parametros);
}
- private static double CalcularSacrificioNormalizado(double valor, double mejor, double peor)
- {
- if (!double.IsFinite(valor) ||
- !double.IsFinite(mejor) ||
- !double.IsFinite(peor) ||
- peor <= mejor)
- {
- return 0;
- }
-
- return Math.Clamp((valor - mejor) / (peor - mejor), 0.0, 1.0);
- }
+ private static double CalcularDistanciaAndandoCorregidaMetros(
+ AlternativaRuta alternativa,
+ ParametrosPlanificadorRuta parametros)
+ => UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieInicioAprox, parametros)
+ + UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieTransbordosAprox, parametros)
+ + UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaMetros(alternativa.DistanciaPieFinAprox, parametros);
private static double DuracionVisibleOrdenacion(AlternativaRuta alternativa)
=> double.IsFinite(alternativa.DuracionTotalAproxSegundos)
@@ -3049,6 +3097,7 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
{
var duracionCandidata = DuracionVisibleOrdenacion(candidata);
var pieCandidata = candidata.DistanciaPieTotalAprox;
+ var busCandidata = candidata.AlternativaBus?.DistanciaTotalBus ?? double.PositiveInfinity;
foreach (var otra in alternativas)
{
@@ -3060,9 +3109,11 @@ public sealed class PlanificadorRutasServicio : IPlanificadorRutas
var duracionOtra = DuracionVisibleOrdenacion(otra);
var pieOtra = otra.DistanciaPieTotalAprox;
+ var busOtra = otra.AlternativaBus?.DistanciaTotalBus ?? double.PositiveInfinity;
var noPierdeTiempo = duracionOtra <= duracionCandidata + ToleranciaDominanciaFinalSegundos;
var noPierdeAndando = pieOtra <= pieCandidata + ToleranciaDominanciaFinalMetros;
- if (!noPierdeTiempo || !noPierdeAndando)
+ var noPierdeBus = busOtra <= busCandidata + ToleranciaDominanciaFinalBusMetros;
+ if (!noPierdeTiempo || !noPierdeAndando || !noPierdeBus)
continue;
var mejoraTransbordo = otra.NumeroTransbordosReales < candidata.NumeroTransbordosReales;
diff --git a/RutasDBUS/wwwroot/app.css b/RutasDBUS/wwwroot/app.css
index 1a2ea84..76efeda 100644
--- a/RutasDBUS/wwwroot/app.css
+++ b/RutasDBUS/wwwroot/app.css
@@ -807,6 +807,74 @@ html, body {
background: #2563eb;
}
+.route-visual-options {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ margin: -.15rem 0 .4rem;
+}
+
+.route-elevation-switch {
+ display: inline-flex;
+ align-items: center;
+ gap: .38rem;
+ min-height: 24px;
+ color: #cbd5e1;
+ font-size: .7rem;
+ font-weight: 700;
+ cursor: pointer;
+ pointer-events: auto;
+ user-select: none;
+}
+
+.route-elevation-switch input {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.route-elevation-switch-track {
+ position: relative;
+ width: 34px;
+ height: 18px;
+ flex: 0 0 34px;
+ border-radius: 999px;
+ border: 1px solid rgba(148, 163, 184, .5);
+ background: rgba(2, 6, 23, .7);
+ transition: background .15s ease, border-color .15s ease;
+}
+
+.route-elevation-switch-thumb {
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 12px;
+ height: 12px;
+ border-radius: 999px;
+ background: #94a3b8;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
+ transition: transform .15s ease, background .15s ease;
+}
+
+.route-elevation-switch--active {
+ color: #f8fafc;
+}
+
+.route-elevation-switch--active .route-elevation-switch-track {
+ border-color: rgba(251, 191, 36, .85);
+ background: linear-gradient(90deg, rgba(56, 189, 248, .75), rgba(249, 115, 22, .85));
+}
+
+.route-elevation-switch--active .route-elevation-switch-thumb {
+ transform: translateX(16px);
+ background: #f8fafc;
+}
+
+.route-elevation-switch:has(input:disabled) {
+ cursor: default;
+ opacity: .55;
+}
+
/* Lista de alternativas de ruta */
.route-alt-list {
@@ -872,12 +940,6 @@ html, body {
font-weight: 800;
}
-.route-alt-balance {
- color: #bfdbfe;
- white-space: nowrap;
- font-weight: 700;
-}
-
/* Separador fino dentro del panel */
.route-panel-divider {
@@ -1549,6 +1611,19 @@ html, body {
text-align: center;
}
+.config-dual-labeled--equilibrado {
+ grid-template-columns: minmax(0, 4rem) minmax(0, 4.4rem);
+ align-items: end;
+}
+
+.config-equilibrado-help {
+ align-self: end;
+ padding-bottom: .12rem;
+ font-size: .58rem;
+ line-height: 1.08;
+ text-align: left;
+}
+
.num-stepper {
@@ -1870,11 +1945,6 @@ html, body {
min-width: 0;
}
-.line-filter__range--segundos-parada {
- grid-template-columns: auto minmax(120px, 150px);
- margin-top: .5rem;
-}
-
.line-filter__selector-head {
display: flex;
align-items: center;
@@ -2023,6 +2093,65 @@ html, body {
pointer-events: none;
}
+.leaflet-tooltip.rt-elev-tooltip {
+ background: rgba(15, 23, 42, .92) !important;
+ border: 1px solid rgba(226, 232, 240, .35) !important;
+ border-radius: 6px !important;
+ color: #f8fafc !important;
+ font-size: .68rem !important;
+ font-weight: 800 !important;
+ padding: 2px 6px !important;
+ box-shadow: 0 8px 18px rgba(0, 0, 0, .35) !important;
+}
+
+.leaflet-tooltip.rt-elev-tooltip:before {
+ display: none !important;
+}
+
+.rt-route-elev-point {
+ stroke: #020617 !important;
+ stroke-width: 1.8px !important;
+ filter: drop-shadow(0 1px 2px rgba(2, 6, 23, .75));
+}
+
+.rt-route-elev-point--subida,
+.rt-route-elev-point--subida-fuerte {
+ animation: rt-route-elev-pulse-up 1.45s ease-in-out infinite;
+}
+
+.rt-route-elev-point--bajada,
+.rt-route-elev-point--bajada-fuerte {
+ animation: rt-route-elev-pulse-down 1.45s ease-in-out infinite;
+}
+
+.rt-route-elev-point--llano {
+ opacity: .88;
+}
+
+@keyframes rt-route-elev-pulse-up {
+ 0%, 100% {
+ stroke-width: 1.8px;
+ opacity: .95;
+ }
+
+ 50% {
+ stroke-width: 4px;
+ opacity: .72;
+ }
+}
+
+@keyframes rt-route-elev-pulse-down {
+ 0%, 100% {
+ stroke-width: 1.8px;
+ opacity: .95;
+ }
+
+ 50% {
+ stroke-width: 3.5px;
+ opacity: .68;
+ }
+}
+
diff --git a/RutasDBUS/wwwroot/app.js b/RutasDBUS/wwwroot/app.js
index 37b2a4a..0547fd3 100644
--- a/RutasDBUS/wwwroot/app.js
+++ b/RutasDBUS/wwwroot/app.js
@@ -103,6 +103,7 @@ window.rt.clearRoutes = function () {
if (!window.rt.ensureRouteLayer()) return;
window.rt._routeLayer.clearLayers();
window.rt._routeLines = {};
+ if (window.rt.clearRouteElevPoints) window.rt.clearRouteElevPoints();
if (window.rt._routeStopsLayer) {
window.rt._routeStopsLayer.clearLayers();
@@ -112,13 +113,11 @@ window.rt.clearRoutes = function () {
window.rt.drawRoute = function (id, coords, options) {
if (!window.rt.ensureRouteLayer()) return;
- // borrar línea anterior si existe
if (window.rt._routeLines[id]) {
window.rt._routeLayer.removeLayer(window.rt._routeLines[id]);
delete window.rt._routeLines[id];
}
- // borrar flecha anterior si existe
const arrowId = id + "__arrow";
if (window.rt._routeLines[arrowId]) {
window.rt._routeLayer.removeLayer(window.rt._routeLines[arrowId]);
@@ -128,38 +127,40 @@ window.rt.drawRoute = function (id, coords, options) {
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 arrowClass = dashArray ? "rt-arrow rt-arrow--dashed" : "rt-arrow";
- const arrowInnerHtml = dashArray ? '' : '';
+
+ 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;
+ };
const lineOpts = { color, weight, opacity };
if (dashArray) lineOpts.dashArray = dashArray;
if (lineCap) lineOpts.lineCap = lineCap;
- const line = window.L.polyline(coords, lineOpts);
-
- // ✅ Hacer la línea clicable y avisar a Blazor (igual que drawPreviewLine)
- 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 line = buildLine(coords, lineOpts);
line.addTo(window.rt._routeLayer);
window.rt._routeLines[id] = line;
- // Flecha SOLO al final
if (arrowEnd && coords && coords.length >= 2) {
const a = coords[coords.length - 2];
const b = coords[coords.length - 1];
@@ -174,6 +175,9 @@ window.rt.drawRoute = function (id, coords, options) {
const size = Math.max(14, Math.min(28, weight * 4)); // tamaño ligado al grosor
+ const arrowClass = dashArray ? "rt-arrow rt-arrow--dashed" : "rt-arrow";
+ const arrowInnerHtml = dashArray ? '' : '';
+
const icon = window.L.divIcon({
className: "rt-arrow-icon",
html: ` {
+ const group = window.rt._elevGroups[id];
+ if (group) window.rt._elevLayer.removeLayer(group);
+ delete window.rt._elevGroups[id];
+ });
+};
+
// points: [{ lat, lon, label }]
// options: { radius, opacity, fillOpacity, showLabels, labelEvery }
window.rt.drawElevPoints = function (id, points, options) {
@@ -294,18 +310,28 @@ window.rt.drawElevPoints = function (id, points, options) {
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
+ fillOpacity: fillOpacity,
+ color: pointColor,
+ fillColor: pointColor,
+ className: className
});
if (showLabels && every > 0 && (i % every === 0)) {
@@ -342,6 +368,15 @@ window.rt.drawElevPoints = function (id, points, options) {
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.addTo(group);
}