import { Component, OnInit, OnDestroy, AfterViewInit, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import * as L from 'leaflet';
import { RouteState } from '../../../core/route-state/route';
import { OrsService } from '../../../core/services/ors.service';
import { RouteAssignedResponse } from '../../../core/models/route.model';
import { sequenceRoutes } from '../../../shared/utils/route-sequencing.utils';
@Component({
standalone: true,
selector: 'app-assigned-route-detail',
imports: [CommonModule],
templateUrl: './assigned-route-detail.component.html',
styleUrl: './assigned-route-detail.component.css'
})
export class AssignedRouteDetailComponent implements OnInit, OnDestroy, AfterViewInit {
private map!: L.Map;
private tileLayer!: L.TileLayer;
private routeGlow?: L.Layer;
private routeLine?: L.Layer;
private arrowLayer?: L.Layer;
private markers: L.Marker[] = [];
private sub?: Subscription;
private destroyed = false;
routeData: RouteAssignedResponse | null = null;
isLoading = false;
error: string | null = null;
segmentOriginName = '';
visitOrder = 0;
totalStops = 0;
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private routeState: RouteState,
private orsService: OrsService,
private cdr: ChangeDetectorRef
) {}
ngOnInit() {
this.sub = this.routeState.selectedRoute$.subscribe((route: RouteAssignedResponse | null) => {
if (route) {
this.routeData = route;
if (this.map) {
this.loadAndShowRoute(route);
}
this.error = null;
} else {
// Try to resolve from queryParams + sessionStorage snapshot
const qp = this.activatedRoute.snapshot.queryParams;
const pdvId = qp['pdv_id'];
const usuarioId = qp['usuario_id'];
if (pdvId) {
const found = this.routeState.findRoute(pdvId, usuarioId);
if (found) {
this.routeData = found;
if (this.map) this.loadAndShowRoute(found);
this.error = null;
} else {
// No data at all (e.g., direct URL after hard refresh with empty storage)
this.error = 'No hay ruta seleccionada. Vuelve al listado y selecciona una ruta.';
}
} else if (!this.routeData) {
this.error = 'No hay ruta seleccionada';
}
}
this.cdr.detectChanges();
});
}
ngAfterViewInit() {
this.initMap();
if (this.routeData) {
this.loadAndShowRoute(this.routeData);
} else {
// Re-attempt resolution after map init (queryParams case)
const qp = this.activatedRoute.snapshot.queryParams;
if (qp['pdv_id']) {
const found = this.routeState.findRoute(qp['pdv_id'], qp['usuario_id']);
if (found) {
this.routeData = found;
this.error = null;
this.loadAndShowRoute(found);
this.cdr.detectChanges();
}
}
}
}
ngOnDestroy() {
this.destroyed = true;
this.sub?.unsubscribe();
this.clearRoute();
if (this.map) {
this.map.remove();
}
}
private initMap() {
this.map = L.map('map').setView([18.4861, -69.9312], 13);
const pdvPane = this.map.createPane('pdvPane');
pdvPane.style.zIndex = '650';
this.tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
}
private async loadAndShowRoute(route: RouteAssignedResponse) {
this.clearRoute();
this.isLoading = true;
this.error = null;
this.cdr.detectChanges();
const json = route.json;
const sequence = sequenceRoutes(this.routeState.getAssignedSnapshot().filter(item =>
String(item.json.usuario_id) === String(json.usuario_id) && item.json.dia === json.dia
));
const segment = sequence.find(item => item.route === route) ?? sequence.find(item => item.route.json.pdv_id === json.pdv_id);
const origin: [number, number] = segment?.origin ?? [Number(json.longitud_usuario), Number(json.latitud_usuario)];
const destination: [number, number] = [Number(json.longitud_pdv), Number(json.latitud_pdv)];
this.segmentOriginName = segment?.previousPdvName ?? `Casa de ${json.nombre_usuario}`;
this.visitOrder = segment?.order ?? 1;
this.totalStops = segment?.totalStops ?? 1;
this.centerMapOnPoints(origin, destination);
try {
const results = await this.orsService.processPairs(
[{ pdv_id: json.pdv_id, origin, destination }],
1,
true
);
if (this.destroyed) return;
const result = results[0] as { error?: string; geojson?: unknown } | undefined;
if (result && !result.error && result.geojson) {
this.showRouteOnMap(result.geojson, origin, destination, json, this.segmentOriginName);
} else {
this.showFallbackRoute(origin, destination, json, this.segmentOriginName);
}
} catch {
this.showFallbackRoute(origin, destination, json, this.segmentOriginName);
} finally {
this.isLoading = false;
this.cdr.detectChanges();
}
}
private centerMapOnPoints(origin: [number, number], destination: [number, number]) {
const bounds = L.latLngBounds([
L.latLng(origin[1], origin[0]),
L.latLng(destination[1], destination[0])
]);
this.map.fitBounds(bounds.pad(0.3));
}
private extractCoords(geojson: unknown): L.LatLngExpression[] {
const g = geojson as { type: string; features?: { geometry: { coordinates: number[][] } }[]; coordinates?: number[][] };
if (g.type === 'FeatureCollection' && g.features?.length) {
return g.features[0].geometry.coordinates.map(
(c: number[]) => [c[1], c[0]] as L.LatLngExpression
);
}
if (g.type === 'LineString' && g.coordinates) {
return g.coordinates.map(
(c: number[]) => [c[1], c[0]] as L.LatLngExpression
);
}
// ORS geojson response wrapped
const maybeFeature = (geojson as { features?: { geometry: { coordinates: number[][] } }[] })?.features;
if (maybeFeature?.length) {
return maybeFeature[0].geometry.coordinates.map(c => [c[1], c[0]] as L.LatLngExpression);
}
return [];
}
private showRouteOnMap(geojson: unknown, origin: [number, number], destination: [number, number], json: RouteAssignedResponse['json'], originName: string) {
const coords = this.extractCoords(geojson);
if (!coords.length) {
this.showFallbackRoute(origin, destination, json, originName);
return;
}
const g = geojson as { features?: { properties?: { summary?: { distance: number; duration: number } } }[] };
const distance = g.features?.[0]?.properties?.summary?.distance
? (g.features[0].properties.summary.distance / 1000).toFixed(2) : 'N/A';
const duration = g.features?.[0]?.properties?.summary?.duration
? (g.features[0].properties.summary.duration / 60).toFixed(0) : 'N/A';
this.routeGlow = L.polyline(coords, {
color: '#6CC24A',
weight: 12,
opacity: 0.35,
lineCap: 'round',
lineJoin: 'round'
}).addTo(this.map);
this.routeLine = L.polyline(coords, {
color: '#6CC24A',
weight: 6,
opacity: 0.95,
lineCap: 'round',
lineJoin: 'round'
}).addTo(this.map);
this.arrowLayer = L.polyline(coords, {
color: '#ffffff',
weight: 4,
opacity: 0.7,
dashArray: '2, 16',
dashOffset: '0',
lineCap: 'round'
}).addTo(this.map);
(this.routeLine as L.Polyline).bindPopup(
`${originName} → ${json.nombre_pdv}
Distancia: ${distance} km
Duración: ${duration} min`
);
this.addMarkers(origin, destination, json, distance, duration, originName);
const group = new L.FeatureGroup([
...(this.routeGlow ? [this.routeGlow] : []),
...(this.routeLine ? [this.routeLine] : []),
...this.markers
]);
this.map.fitBounds(group.getBounds().pad(0.1));
}
private showFallbackRoute(origin: [number, number], destination: [number, number], json: RouteAssignedResponse['json'], originName: string) {
this.routeLine = L.polyline(
[[origin[1], origin[0]], [destination[1], destination[0]]],
{ color: '#FF6A13', weight: 5, dashArray: '12, 10', opacity: 0.9 }
).addTo(this.map);
this.addMarkers(origin, destination, json, 'N/A', 'N/A', originName);
const group = new L.FeatureGroup([this.routeLine, ...this.markers]);
this.map.fitBounds(group.getBounds().pad(0.1));
}
private addMarkers(
origin: [number, number],
destination: [number, number],
json: RouteAssignedResponse['json'],
distance: string,
duration: string,
originName: string
) {
const startIcon = L.divIcon({
className: 'custom-marker marker-start',
html: '