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: '
A
', iconSize: [30, 30], iconAnchor: [15, 15], popupAnchor: [0, -18] }); const endIcon = L.divIcon({ className: 'custom-marker marker-end', html: '
', iconSize: [30, 38], iconAnchor: [15, 38], popupAnchor: [0, -18] }); this.markers.push( L.marker([origin[1], origin[0]], { icon: startIcon }) .addTo(this.map) .bindPopup(`Origen del tramo: ${originName}`), L.marker([destination[1], destination[0]], { icon: endIcon, pane: 'pdvPane', zIndexOffset: 1000 }) .addTo(this.map) .bindPopup(`Destino: ${json.nombre_pdv}
Usuario: ${json.nombre_usuario}
Distancia: ${distance} km
Duración: ${duration} min`) ); } private clearRoute() { if (this.routeGlow) { this.map.removeLayer(this.routeGlow); this.routeGlow = undefined; } if (this.routeLine) { this.map.removeLayer(this.routeLine); this.routeLine = undefined; } if (this.arrowLayer) { this.map.removeLayer(this.arrowLayer); this.arrowLayer = undefined; } this.markers.forEach(m => this.map.removeLayer(m)); this.markers = []; } goBack() { this.router.navigate(['/home']); } }