import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../core/app_theme.dart'; import '../providers/monitoring_provider.dart'; import '../widgets/isometric_transform.dart'; import '../widgets/isometric_kumbung_painter.dart'; class KumbungMapPage extends ConsumerStatefulWidget { const KumbungMapPage({super.key}); @override ConsumerState createState() => _KumbungMapPageState(); } class _KumbungMapPageState extends ConsumerState with SingleTickerProviderStateMixin { late AnimationController _animationController; // Koordinat Sensor (X, Y, Z) dalam meter, dan Radius Jangkauan (meter) double _sensorX = 4.0; double _sensorY = 3.0; final double _sensorZ = 1.2; // Tinggi melayang sensor di rak final double _sensorRadius = 2.0; // Radius jangkauan fiks 2.0 meter // Mode Heatmap: true = Suhu, false = Kelembapan bool _isSuhuMode = true; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); _loadSensorSettings(); } @override void dispose() { _animationController.dispose(); super.dispose(); } // Memuat setelan koordinat lokal dari SharedPreferences Future _loadSensorSettings() async { final prefs = await SharedPreferences.getInstance(); setState(() { _sensorX = (prefs.getDouble('sensor_x') ?? 4.0).clamp(0.5, 7.5); _sensorY = (prefs.getDouble('sensor_y') ?? 3.0).clamp(0.5, 5.5); }); } // Menyimpan setelan koordinat lokal ke SharedPreferences Future _saveSensorSettings() async { final prefs = await SharedPreferences.getInstance(); await prefs.setDouble('sensor_x', _sensorX); await prefs.setDouble('sensor_y', _sensorY); } // Mengubah posisi sensor saat canvas diketuk oleh pengguna void _handleCanvasTap(Offset localPosition, Size canvasSize, IsometricTransform transformer) { final worldPt = transformer.toWorldGround(localPosition); // Batasi agar koordinat berada di dalam rentang slider (0.5 s.d 7.5 untuk X, 0.5 s.d 5.5 untuk Y) // guna menghindari crash assertion Slider jika user mengetuk area luar grid lantai. final double clampedX = worldPt.dx.clamp(0.5, 7.5); final double clampedY = worldPt.dy.clamp(0.5, 5.5); setState(() { _sensorX = clampedX; _sensorY = clampedY; }); _saveSensorSettings(); } @override Widget build(BuildContext context) { final monitoring = ref.watch(monitoringProvider); final hasDevice = monitoring.latest != null; // Nilai telemetri sensor real-time final double tempVal = monitoring.temperature; final double humidVal = monitoring.humidity; // Tentukan warna status untuk sensor berdasarkan nilainya final Color pinColor = _isSuhuMode ? (tempVal > 30 ? Colors.redAccent : Colors.teal) : (humidVal < 75 ? Colors.orangeAccent : Colors.cyan); return Scaffold( backgroundColor: AppTheme.bgPage, body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Header Section ───────────────────────────────── Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Peta Kumbung', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), Text( 'Denah & Cakupan Jangkauan Sensor', style: TextStyle( fontSize: 13, color: AppTheme.textSecondary, ), ), ], ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: hasDevice ? Colors.green.shade50 : Colors.grey.shade100, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ CircleAvatar( radius: 4, backgroundColor: hasDevice ? Colors.green : Colors.grey, ), const SizedBox(width: 6), Text( hasDevice ? 'Sensor Aktif' : 'Sensor Offline', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: hasDevice ? Colors.green.shade700 : Colors.grey.shade700, ), ), ], ), ), ], ), const SizedBox(height: 16), // ── Visualizer Canvas 3D ─────────────────────────── Container( width: double.infinity, height: 280, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.03), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: Stack( children: [ // Canvas Utama Rendering Isometrik LayoutBuilder( builder: (context, constraints) { const double roomWidth = 6.0; const double roomLength = 8.0; const double roomHeight = 2.5; const double scale = 24.0; // Menghitung origin tengah agar visualisasi isometrik berada tepat di pusat canvas const double angle = 30.0 * math.pi / 180.0; final double cos30 = math.cos(angle); final double sin30 = math.sin(angle); final double dxCenterOffset = (roomLength - roomWidth) / 2 * cos30 * scale; final double dyCenterOffset = ((roomLength + roomWidth) * sin30 - roomHeight) / 2 * scale; final centerOrigin = Offset( constraints.maxWidth / 2 - dxCenterOffset, constraints.maxHeight / 2 - dyCenterOffset, ); // Skala dinamis: 1 meter = 24 piksel final transformer = IsometricTransform( scale: scale, origin: centerOrigin, ); return GestureDetector( onTapUp: (details) => _handleCanvasTap( details.localPosition, Size(constraints.maxWidth, constraints.maxHeight), transformer, ), child: AnimatedBuilder( animation: _animationController, builder: (context, child) { return CustomPaint( size: Size(constraints.maxWidth, constraints.maxHeight), painter: StaticKumbungPainter( transformer: transformer, roomWidth: roomWidth, roomLength: roomLength, roomHeight: roomHeight, ), foregroundPainter: PinKumbungPainter( transformer: transformer, sensorPos: Offset(_sensorX, _sensorY), sensorHeight: _sensorZ, value: _isSuhuMode ? tempVal : humidVal, unit: _isSuhuMode ? '°C' : '%', animationValue: _animationController.value, pinColor: pinColor, ), child: CustomPaint( size: Size(constraints.maxWidth, constraints.maxHeight), painter: HeatmapKumbungPainter( transformer: transformer, sensorPos: Offset(_sensorX, _sensorY), radius: _sensorRadius, value: _isSuhuMode ? tempVal : humidVal, isSuhuMode: _isSuhuMode, ), ), ); }, ), ); }, ), // Toggle Switch Peta Suhu / Kelembapan Positioned( top: 12, right: 12, child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(16), ), child: Row( children: [ _buildToggleButton( label: 'Suhu', isActive: _isSuhuMode, color: Colors.teal, onTap: () => setState(() => _isSuhuMode = true), ), _buildToggleButton( label: 'Kelembapan', isActive: !_isSuhuMode, color: Colors.cyan, onTap: () => setState(() => _isSuhuMode = false), ), ], ), ), ), // Instruksi Interaksi const Positioned( bottom: 12, left: 12, child: Row( children: [ Icon(Icons.touch_app, size: 14, color: AppTheme.textSecondary), SizedBox(width: 4), Text( 'Ketuk lantai grid untuk memindah sensor', style: TextStyle( fontSize: 10, color: AppTheme.textSecondary, ), ), ], ), ), ], ), ), ), const SizedBox(height: 16), // ── Dashboard Referensi Budidaya ─────────────────── _buildReferenceCard(), ], ), ), ), ); } // Widget Button Toggle Widget _buildToggleButton({ required String label, required bool isActive, required Color color, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isActive ? color : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Text( label, style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: isActive ? Colors.white : AppTheme.textSecondary, ), ), ), ); } // Widget Kartu Referensi Budidaya Jamur Widget _buildReferenceCard() { return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.02), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppTheme.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: const Icon(Icons.menu_book, color: AppTheme.primary, size: 16), ), const SizedBox(width: 10), const Text( 'Panduan Referensi Budidaya', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), ], ), const SizedBox(height: 12), const Text( 'Untuk budidaya Jamur Tiram yang optimal, berikut adalah parameter referensi kondisi kumbung:', style: TextStyle(fontSize: 12, color: AppTheme.textSecondary, height: 1.4), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildRefIndicator( icon: Icons.thermostat, iconColor: Colors.teal, label: 'Suhu Ideal', value: '20°C - 33°C', ), ), const SizedBox(width: 12), Expanded( child: _buildRefIndicator( icon: Icons.water_drop, iconColor: Colors.cyan, label: 'Kelembapan Ideal', value: '80% - 90%', ), ), ], ), const SizedBox(height: 12), Divider(color: Colors.grey.shade100), const SizedBox(height: 8), const Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.info_outline, size: 16, color: AppTheme.primary), SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Informasi Suhu & Kelembapan:', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), SizedBox(height: 4), Text( 'Suhu yang stabil mencegah stres pada tubuh buah jamur, sedangkan kelembapan yang tinggi (di atas 80%) sangat penting untuk memicu pertumbuhan pinhead (primordia) secara maksimal.', style: TextStyle(fontSize: 11, color: AppTheme.textSecondary, height: 1.5), ), ], ), ), ], ), ], ), ); } Widget _buildRefIndicator({ required IconData icon, required Color iconColor, required String label, required String value, }) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: iconColor.withValues(alpha: 0.1)), ), child: Row( children: [ Icon(icon, color: iconColor, size: 20), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle(fontSize: 9, color: AppTheme.textSecondary), ), const SizedBox(height: 2), Text( value, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: iconColor.withValues(alpha: 0.9), ), ), ], ), ], ), ); } }