TKK_E32230206/lib/features/iot/page/kumbung_map_page.dart

587 lines
21 KiB
Dart

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<KumbungMapPage> createState() => _KumbungMapPageState();
}
class _KumbungMapPageState extends ConsumerState<KumbungMapPage>
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
double _sensorRadius = 3.0;
// 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<void> _loadSensorSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_sensorX = prefs.getDouble('sensor_x') ?? 4.0;
_sensorY = prefs.getDouble('sensor_y') ?? 3.0;
_sensorRadius = prefs.getDouble('sensor_radius') ?? 3.0;
});
}
// Menyimpan setelan koordinat lokal ke SharedPreferences
Future<void> _saveSensorSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble('sensor_x', _sensorX);
await prefs.setDouble('sensor_y', _sensorY);
await prefs.setDouble('sensor_radius', _sensorRadius);
}
// 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 ruangan (8m x 6m)
final double clampedX = worldPt.dx.clamp(0.0, 8.0);
final double clampedY = worldPt.dy.clamp(0.0, 6.0);
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) {
// Menghitung origin tengah bawah untuk visualisasi isometrik
final centerOrigin = Offset(
constraints.maxWidth / 2,
constraints.maxHeight / 2 + 30,
);
// Skala dinamis: 1 meter = 24 piksel
final transformer = IsometricTransform(
scale: 24.0,
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: 6.0,
roomLength: 8.0,
roomHeight: 2.5,
),
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),
// ── Control Settings Panel ─────────────────────────
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.settings_input_hdmi, color: AppTheme.primary, size: 18),
const SizedBox(width: 8),
Text(
'Konfigurasi Sensor Terpasang',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
],
),
const SizedBox(height: 12),
_buildSlider(
label: 'Posisi X (Panjang)',
value: _sensorX,
min: 0.5,
max: 7.5,
displayValue: '${_sensorX.toStringAsFixed(1)}m',
onChanged: (v) {
setState(() => _sensorX = v);
_saveSensorSettings();
},
),
_buildSlider(
label: 'Posisi Y (Lebar)',
value: _sensorY,
min: 0.5,
max: 5.5,
displayValue: '${_sensorY.toStringAsFixed(1)}m',
onChanged: (v) {
setState(() => _sensorY = v);
_saveSensorSettings();
},
),
_buildSlider(
label: 'Radius Jangkauan',
value: _sensorRadius,
min: 1.0,
max: 5.0,
displayValue: '${_sensorRadius.toStringAsFixed(1)}m',
onChanged: (v) {
setState(() => _sensorRadius = v);
_saveSensorSettings();
},
),
],
),
),
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 Row Slider Pengaturan
Widget _buildSlider({
required String label,
required double value,
required double min,
required double max,
required String displayValue,
required ValueChanged<double> onChanged,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(
label,
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
),
Expanded(
flex: 6,
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: AppTheme.primary,
inactiveTrackColor: Colors.grey.shade200,
thumbColor: AppTheme.primary,
trackHeight: 3,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
),
child: Slider(
value: value,
min: min,
max: max,
onChanged: onChanged,
),
),
),
Expanded(
flex: 2,
child: Text(
displayValue,
textAlign: TextAlign.right,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
),
],
),
);
}
// 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.withOpacity(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.withOpacity(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: '22°C - 28°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.wb_sunny_outlined, size: 16, color: Colors.orange),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Waktu Penyiraman Terbaik:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 4),
Text(
'• Pagi Hari (06.00 - 08.00 WIB): Mempersiapkan kelembapan sebelum suhu udara mulai meningkat.\n• Sore Hari (16.00 - 18.00 WIB): Mengembalikan kelembapan setelah penguapan tinggi di siang hari.',
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.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: iconColor.withOpacity(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.withOpacity(0.9),
),
),
],
),
],
),
);
}
}