import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/telemetry_sample.dart'; import '../../rides/ride_state_machine.dart'; import '../../../services/map/map_provider_interface.dart'; import '../../../services/map/unified_map_view.dart'; import '../../../services/alerts/road_alert_models.dart'; import '../../../services/alerts/road_alert_service.dart'; import '../../records/records_service.dart'; class EvolutionLandscapeDashboard extends ConsumerWidget { final double currentSpeedMph; final double heading; final double leanAngle; final double accelG; final double lateralG; final RideSession session; final TelemetrySample? telemetry; const EvolutionLandscapeDashboard({ super.key, required this.currentSpeedMph, required this.heading, required this.leanAngle, required this.accelG, required this.lateralG, required this.session, this.telemetry, }); @override Widget build(BuildContext context, WidgetRef ref) { final records = ref.watch(recordsServiceProvider); final speed = currentSpeedMph.clamp(0.0, 220.0); final centerLat = (telemetry?.latitude != null && telemetry!.latitude != 0.0) ? telemetry!.latitude! : 37.7749; final centerLng = (telemetry?.longitude != null && telemetry!.longitude != 0.0) ? telemetry!.longitude! : -122.4194; final vehicleMarker = AppMapMarker( id: 'landscape_vehicle', position: AppLatLng(centerLat, centerLng), heading: heading, iconWidget: const Icon( Icons.navigation, color: Color(0xFF00E5FF), size: 38, ), ); final activeAlertsAsync = ref.watch(activeAlertsStreamProvider); final activeAlerts = activeAlertsAsync.value ?? []; final mapMarkers = [vehicleMarker]; for (final alert in activeAlerts) { mapMarkers.add( AppMapMarker( id: 'landscape_alert_${alert.providerId}_${alert.id}', position: AppLatLng(alert.latitude, alert.longitude), heading: alert.heading ?? 0.0, label: alert.category.displayName, iconWidget: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: alert.category.color.withValues(alpha: 0.9), shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 4)], ), child: Icon(alert.category.icon, color: Colors.black, size: 16), ), ), ); } return Container( color: Colors.black, child: Row( children: [ // Left Panel (65%): Vector Tactical Route Map Expanded( flex: 65, child: Container( margin: const EdgeInsets.all(8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), ), clipBehavior: Clip.antiAlias, child: Stack( children: [ LayoutBuilder( builder: (context, constraints) { return SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: UnifiedMapView( key: ValueKey('evo_map_${constraints.maxWidth.toInt()}x${constraints.maxHeight.toInt()}'), center: AppLatLng(centerLat, centerLng), zoom: 17.0, markers: mapMarkers, ), ); }, ), Positioned( bottom: 12, left: 12, child: ElevatedButton.icon( onPressed: () { if (session.state == RideState.riding) { ref.read(rideStateMachineProvider.notifier).finishRide(); } else { ref.read(rideStateMachineProvider.notifier).startRide(); } }, style: ElevatedButton.styleFrom( backgroundColor: session.state == RideState.riding ? const Color(0xFF3D0000) : const Color(0xFF00252A), foregroundColor: session.state == RideState.riding ? const Color(0xFFFF2A2A) : const Color(0xFF00E5FF), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide( color: session.state == RideState.riding ? const Color(0xFFFF2A2A) : const Color(0xFF00E5FF), width: 1.5, ), ), elevation: 4, ), icon: Icon( session.state == RideState.riding ? Icons.stop_circle : Icons.play_circle, size: 18, color: session.state == RideState.riding ? const Color(0xFFFF2A2A) : const Color(0xFF00E5FF), ), label: Text( session.state == RideState.riding ? 'STOP SESSION' : 'START SESSION', style: const TextStyle( fontFamily: 'Sora', fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.1, ), ), ), ), ], ), ), ), // Right Panel (35%): Telemetry Stack Expanded( flex: 35, child: Padding( padding: const EdgeInsets.only(top: 8, bottom: 8, right: 8), child: Column( children: [ // 1. G-Force Indicator with Max Accel / Brake Bars Expanded( flex: 4, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xFF0D0D0D), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF222222), width: 1.0), ), child: Column( children: [ const Text( 'G-FORCE INDICATOR', style: TextStyle( fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800, color: Colors.white70, letterSpacing: 0.1, ), ), const SizedBox(height: 4), Expanded( child: Row( children: [ // Accel bar _buildVerticalGBar('ACCEL', accelG > 0 ? accelG : 0.0, const Color(0xFF69F0AE)), // Center Circular Target Expanded( child: CustomPaint( size: const Size(double.infinity, double.infinity), painter: _EvolutionGForcePainter(accelG: accelG, lateralG: lateralG), ), ), // Brake bar _buildVerticalGBar('BRAKE', accelG < 0 ? -accelG : 0.0, const Color(0xFFFF2A2A)), ], ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'MAX ACCEL\n${records.maxAccelG > 0 ? records.maxAccelG.toStringAsFixed(2) : "0.98"}G', textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'JetBrains Mono', fontSize: 8, fontWeight: FontWeight.w700, color: Colors.white54, ), ), Text( 'MAX BRAKE\n${records.maxBrakingG > 0 ? records.maxBrakingG.toStringAsFixed(2) : "1.10"}G', textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'JetBrains Mono', fontSize: 8, fontWeight: FontWeight.w700, color: Colors.white54, ), ), ], ), ], ), ), ), const SizedBox(height: 6), // 2. Lean Angle Indicator Arc Expanded( flex: 3, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: const Color(0xFF0D0D0D), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF222222), width: 1.0), ), child: Column( children: [ const Text( 'LEAN ANGLE INDICATOR', style: TextStyle( fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w800, color: Colors.white70, letterSpacing: 0.1, ), ), Expanded( child: Stack( alignment: Alignment.center, children: [ CustomPaint( size: const Size(double.infinity, double.infinity), painter: _LandscapeLeanPainter(leanAngle: leanAngle), ), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${leanAngle.abs().toStringAsFixed(0)}°', style: const TextStyle( fontFamily: 'JetBrains Mono', fontSize: 20, fontWeight: FontWeight.w900, color: Colors.white, ), ), Text( leanAngle < -2 ? 'LEAN LEFT' : (leanAngle > 2 ? 'LEAN RIGHT' : 'UPRIGHT'), style: TextStyle( fontFamily: 'Inter', fontSize: 8, fontWeight: FontWeight.w800, color: leanAngle.abs() > 2 ? const Color(0xFF00E5FF) : Colors.white38, ), ), ], ), ], ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'L: ${session.maxLeanLeft.toStringAsFixed(0)}°', style: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 9, fontWeight: FontWeight.w700, color: Color(0xFF00E5FF)), ), Text( 'R: ${session.maxLeanRight.toStringAsFixed(0)}°', style: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 9, fontWeight: FontWeight.w700, color: Color(0xFF00E5FF)), ), ], ), ], ), ), ), const SizedBox(height: 6), // 3. Digital Speedometer Cluster Expanded( flex: 3, child: Container( decoration: BoxDecoration( color: const Color(0xFF0A0A0A), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF1F1F1F), width: 1.0), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( speed.toStringAsFixed(0), style: const TextStyle( fontFamily: 'JetBrains Mono', fontSize: 44, fontWeight: FontWeight.w900, fontStyle: FontStyle.italic, color: Colors.white, height: 0.95, shadows: [ Shadow( color: Color(0xFF00E5FF), blurRadius: 12, ), ], ), ), const Text( 'SPEED (MPH)', style: TextStyle( fontFamily: 'Inter', fontSize: 9, fontWeight: FontWeight.w800, color: Color(0xFF00E5FF), letterSpacing: 0.1, ), ), ], ), ), ), ), ], ), ), ), ], ), ); } Widget _buildVerticalGBar(String label, double value, Color color) { final heightFactor = (value / 1.5).clamp(0.0, 1.0); return SizedBox( width: 24, child: Column( children: [ Expanded( child: Container( width: 8, decoration: BoxDecoration( color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(4), ), alignment: Alignment.bottomCenter, child: FractionallySizedBox( heightFactor: heightFactor, child: Container( decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(4), ), ), ), ), ), const SizedBox(height: 4), Text( label, style: const TextStyle( fontFamily: 'Inter', fontSize: 7, fontWeight: FontWeight.w800, color: Colors.white54, ), ), ], ), ); } } class _EvolutionGForcePainter extends CustomPainter { final double accelG; final double lateralG; _EvolutionGForcePainter({required this.accelG, required this.lateralG}); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = math.min(size.width, size.height) * 0.42; final ringPaint = Paint() ..color = const Color(0xFF222222) ..style = PaintingStyle.stroke ..strokeWidth = 1.0; canvas.drawCircle(center, radius * 0.5, ringPaint); canvas.drawCircle(center, radius, ringPaint); final crossPaint = Paint() ..color = Colors.white10 ..strokeWidth = 1.0; canvas.drawLine(Offset(center.dx - radius, center.dy), Offset(center.dx + radius, center.dy), crossPaint); canvas.drawLine(Offset(center.dx, center.dy - radius), Offset(center.dx, center.dy + radius), crossPaint); final dx = (lateralG / 1.5).clamp(-1.0, 1.0) * radius; final dy = (-accelG / 1.5).clamp(-1.0, 1.0) * radius; final dotPos = Offset(center.dx + dx, center.dy + dy); canvas.drawCircle(dotPos, 4.5, Paint()..color = const Color(0xFF00E5FF)); canvas.drawCircle(dotPos, 8.0, Paint()..color = const Color(0xFF00E5FF).withValues(alpha: 0.35)); } @override bool shouldRepaint(covariant _EvolutionGForcePainter oldDelegate) => oldDelegate.accelG != accelG || oldDelegate.lateralG != lateralG; } class _LandscapeLeanPainter extends CustomPainter { final double leanAngle; _LandscapeLeanPainter({required this.leanAngle}); @override void paint(Canvas canvas, Size size) { if (size.width <= 0 || size.height <= 0) return; final center = Offset(size.width / 2, size.height * 0.82); final radius = math.min(size.width * 0.42, size.height * 0.72); if (radius <= 0) return; // Background track arc from 180 to 360 (top half semicircle) final trackPaint = Paint() ..color = const Color(0xFF222222) ..style = PaintingStyle.stroke ..strokeWidth = 6.0 ..strokeCap = StrokeCap.round; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), math.pi, math.pi, false, trackPaint, ); // Tick marks at 0, +-15, +-30, +-45, +-60 final tickPaint = Paint() ..color = Colors.white24 ..strokeWidth = 1.2; for (final angleDeg in [-60, -45, -30, -15, 0, 15, 30, 45, 60]) { final rad = (270 + angleDeg) * (math.pi / 180.0); final p1 = Offset(center.dx + (radius - 5) * math.cos(rad), center.dy + (radius - 5) * math.sin(rad)); final p2 = Offset(center.dx + (radius + 5) * math.cos(rad), center.dy + (radius + 5) * math.sin(rad)); canvas.drawLine(p1, p2, tickPaint); } // Active lean sweep arc from 270 (top/upright) to (270 + leanAngle) final clampedLean = leanAngle.clamp(-65.0, 65.0); if (clampedLean.abs() > 0.5) { final isExtreme = clampedLean.abs() > 45; final activeColor = isExtreme ? const Color(0xFFFF3D00) : const Color(0xFF00E5FF); final activeArcPaint = Paint() ..color = activeColor ..style = PaintingStyle.stroke ..strokeWidth = 6.0 ..strokeCap = StrokeCap.round; final startRad = 270 * (math.pi / 180.0); final sweepRad = clampedLean * (math.pi / 180.0); canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startRad, sweepRad, false, activeArcPaint, ); } // Needle from center to arc final needleRad = (270 + clampedLean) * (math.pi / 180.0); final needleEnd = Offset( center.dx + radius * math.cos(needleRad), center.dy + radius * math.sin(needleRad), ); final needlePaint = Paint() ..color = const Color(0xFF00E5FF) ..strokeWidth = 2.5 ..strokeCap = StrokeCap.round; canvas.drawLine(center, needleEnd, needlePaint); // Glowing tip dot canvas.drawCircle(needleEnd, 4.0, Paint()..color = const Color(0xFF00E5FF)); canvas.drawCircle(needleEnd, 7.0, Paint()..color = const Color(0xFF00E5FF).withValues(alpha: 0.35)); canvas.drawCircle(center, 3.0, Paint()..color = Colors.white70); } @override bool shouldRepaint(covariant _LandscapeLeanPainter oldDelegate) => oldDelegate.leanAngle != leanAngle; }