import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import '../core/app_colors.dart'; import '../models/route_model.dart'; import '../services/route_simulator_service.dart'; List _smooth(List pts) { if (pts.length < 2) return pts; final res = []; for (int i = 0; i < pts.length - 1; i++) { final p0 = pts[i > 0 ? i - 1 : i]; final p1 = pts[i]; final p2 = pts[i + 1]; final p3 = pts[i + 2 < pts.length ? i + 2 : i + 1]; res.add(p1); for (int j = 1; j <= 8; j++) { final t = j / 9.0; res.add(LatLng(_cr(p0.latitude,p1.latitude,p2.latitude,p3.latitude,t), _cr(p0.longitude,p1.longitude,p2.longitude,p3.longitude,t))); } } res.add(pts.last); return res; } double _cr(double a,double b,double c,double d,double t) => 0.5*( (2*b)+(-a+c)*t+(2*a-5*b+4*c-d)*t*t+(-a+3*b-3*c+d)*t*t*t); // Calcular bearing entre dos puntos double _bearing(LatLng from, LatLng to) { final lat1 = from.latitude * math.pi / 180; final lat2 = to.latitude * math.pi / 180; final dLng = (to.longitude - from.longitude) * math.pi / 180; final y = math.sin(dLng) * math.cos(lat2); final x = math.cos(lat1)*math.sin(lat2) - math.sin(lat1)*math.cos(lat2)*math.cos(dLng); return (math.atan2(y, x) * 180 / math.pi + 360) % 360; } // ── Mapa ciudadano ──────────────────────────────────────────────────────── class RouteMapWidget extends StatelessWidget { final RouteModel route; final RouteSimulatorService simulator; final bool showFullRoute; final double height; const RouteMapWidget({super.key, required this.route, required this.simulator, this.showFullRoute = false, this.height = 300}); @override Widget build(BuildContext context) { final posIdx = simulator.getPositionIndex(route.routeId); final cur = posIdx < route.positions.length ? route.positions[posIdx].latLng : route.positions.last.latLng; final gps = simulator.isGpsActive(route.routeId); final all = _smooth(route.polylinePoints); final done = posIdx > 0 ? _smooth(route.positions.take(posIdx+1).map((p)=>p.latLng).toList()) : []; // Calcular bearing hacia siguiente punto double bear = 0; if (posIdx < route.positions.length - 1) { bear = _bearing(cur, route.positions[posIdx+1].latLng); } return ClipRRect( borderRadius: BorderRadius.circular(12), child: SizedBox(height: height, child: FlutterMap( options: MapOptions( initialCenter: cur, initialZoom: 14.5, initialRotation: -bear, // Rotar mapa según dirección del camión ), children: [ TileLayer(urlTemplate:'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName:'com.example.celaya_limpia'), // Ruta completa (gris punteada) PolylineLayer(polylines:[ Polyline(points:all, color:Colors.grey.shade400, strokeWidth:4, borderColor:Colors.white54, borderStrokeWidth:1), ]), // Tramo recorrido (guinda) if (done.isNotEmpty) PolylineLayer(polylines:[ Polyline(points:done, color:AppColors.guindaPrimary, strokeWidth:5, borderColor:AppColors.guindaDark, borderStrokeWidth:1), ]), MarkerLayer(markers:[ // Camión con orientación Marker(point:cur, width:56, height:56, child:Transform.rotate( angle: bear * math.pi / 180, child:Container( decoration:BoxDecoration( color:gps?AppColors.guindaPrimary:Colors.grey, shape:BoxShape.circle, border:Border.all(color:Colors.white,width:2.5), boxShadow:[BoxShadow(color:Colors.black38,blurRadius:6)]), child:Icon(gps?Icons.navigation:Icons.gps_off, color:Colors.white,size:26)))), // Origen Marker(point:route.positions.first.latLng, width:24, height:24, child:Container(decoration:BoxDecoration(color:AppColors.verdeExito, shape:BoxShape.circle,border:Border.all(color:Colors.white,width:2)), child:const Icon(Icons.circle,color:Colors.white,size:8))), // Destino Marker(point:route.positions.last.latLng, width:28, height:28, child:Container(decoration:BoxDecoration(color:AppColors.rojoError, shape:BoxShape.circle,border:Border.all(color:Colors.white,width:2)), child:const Icon(Icons.flag,color:Colors.white,size:14))), ]), ]))); } } // ── Mapa Admin (todas las rutas) ────────────────────────────────────────── class AdminMapWidget extends StatefulWidget { final List routes; final RouteSimulatorService simulator; const AdminMapWidget({super.key, required this.routes, required this.simulator}); @override State createState() => _AdminMapWidgetState(); } class _AdminMapWidgetState extends State { String? _sel; static const _colors = [ Colors.blue,Colors.green,Colors.orange,Colors.purple,Colors.teal, Colors.red,Colors.indigo,Colors.brown,Colors.cyan,Colors.pink, Colors.amber,Colors.lime,Colors.deepOrange,Colors.lightBlue,Colors.deepPurple, ]; @override Widget build(BuildContext context) { return Column(children:[ Expanded(child: FlutterMap( options: const MapOptions(initialCenter:LatLng(20.5211,-100.8196), initialZoom:12), children:[ TileLayer(urlTemplate:'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName:'com.example.celaya_limpia'), PolylineLayer(polylines: widget.routes.asMap().entries.map((e){ final c = _colors[e.key%_colors.length]; final isS = _sel==null||_sel==e.value.routeId; return Polyline(points:_smooth(e.value.polylinePoints), color:c.withOpacity(isS?0.85:0.2), strokeWidth:isS?4:1.5, borderColor:Colors.white.withOpacity(isS?0.4:0), borderStrokeWidth:1); }).toList()), MarkerLayer(markers: widget.routes.asMap().entries.map((e){ final r = e.value; final idx = widget.simulator.getPositionIndex(r.routeId); final pos = idx < r.positions.length ? r.positions[idx].latLng : r.positions.last.latLng; final c = _colors[e.key%_colors.length]; final gps = widget.simulator.isGpsActive(r.routeId); double bear = 0; if (idx < r.positions.length - 1) bear = _bearing(pos, r.positions[idx+1].latLng); return Marker(point:pos, width:44, height:44, child:GestureDetector( onTap:()=>setState(()=>_sel=_sel==r.routeId?null:r.routeId), child:Transform.rotate(angle: bear*math.pi/180, child:Container(decoration:BoxDecoration( color:gps?c:Colors.grey, shape:BoxShape.circle, border:Border.all(color:Colors.white,width:2), boxShadow:[BoxShadow(color:Colors.black26,blurRadius:4)]), child:Center(child:Text(r.truckId.toString().substring(1), style:const TextStyle(color:Colors.white,fontSize:11,fontWeight:FontWeight.bold))))))); }).toList()), ])), if (_sel!=null) Container( padding:const EdgeInsets.symmetric(horizontal:16,vertical:10), color:AppColors.guindaPrimary, child:Row(children:[ const Icon(Icons.local_shipping,color:Colors.white,size:16), const SizedBox(width:8), Expanded(child:Text(widget.routes.firstWhere((r)=>r.routeId==_sel).name, style:const TextStyle(color:Colors.white,fontWeight:FontWeight.bold,fontSize:13))), Text('Pos ${widget.simulator.getPositionIndex(_sel!)}/7', style:const TextStyle(color:AppColors.dorado,fontSize:12)), ])), ]); } } // ── Mapa conductor (con bearing) ────────────────────────────────────────── class DriverRouteMap extends StatelessWidget { final RouteModel route; final RouteSimulatorService simulator; const DriverRouteMap({super.key, required this.route, required this.simulator}); @override Widget build(BuildContext context) { final idx = simulator.getPositionIndex(route.routeId); final gps = simulator.isGpsActive(route.routeId); final positions = route.positions; final cur = idx < positions.length ? positions[idx].latLng : positions.last.latLng; double bear = 0; if (idx < positions.length - 1) bear = _bearing(cur, positions[idx+1].latLng); final done = idx > 0 ? _smooth(positions.take(idx+1).map((p)=>p.latLng).toList()) : []; final pending = _smooth(positions.skip(idx).map((p)=>p.latLng).toList()); return FlutterMap( options: MapOptions(initialCenter:cur, initialZoom:15, initialRotation:-bear), children:[ TileLayer(urlTemplate:'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName:'com.example.celaya_limpia'), PolylineLayer(polylines:[ Polyline(points:pending, color:Colors.grey.shade400, strokeWidth:5, borderColor:Colors.white54, borderStrokeWidth:1), if (done.isNotEmpty) Polyline(points:done, color:AppColors.guindaPrimary, strokeWidth:6, borderColor:AppColors.guindaPrimary.withOpacity(0.4), borderStrokeWidth:2), ]), MarkerLayer(markers:[ // Waypoints pendientes ...positions.skip(idx+1).take(4).toList().asMap().entries.map((e)=> Marker(point:e.value.latLng, width:22, height:22, child:Container(decoration:BoxDecoration(color:Colors.amber,shape:BoxShape.circle, border:Border.all(color:Colors.white,width:1.5)), child:Center(child:Text('${idx+2+e.key}', style:const TextStyle(color:Colors.white,fontSize:9,fontWeight:FontWeight.bold)))))), // Camión orientado Marker(point:cur, width:56, height:56, child:Transform.rotate(angle:bear*math.pi/180, child:Container(decoration:BoxDecoration( color:gps?AppColors.guindaPrimary:Colors.grey, shape:BoxShape.circle, border:Border.all(color:Colors.white,width:2.5), boxShadow:[BoxShadow(color:Colors.black38,blurRadius:8)]), child:Icon(gps?Icons.navigation:Icons.gps_off,color:Colors.white,size:28)))), ]), ]); } }