This commit is contained in:
parent
726d561670
commit
f9e5ab02c7
|
|
@ -5,7 +5,7 @@ import 'package:firebase_database/firebase_database.dart';
|
|||
import 'dart:async';
|
||||
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
||||
import '../../monitoring/evaporasi_monitoring_screen.dart';
|
||||
import '../../monitoring/wind_speed_monitoring_screen.dart';
|
||||
import '../../monitoring/wind_speed/wind_speed_monitoring_screen.dart';
|
||||
import '../../monitoring/air_quality_monitoring_screen.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,225 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
|
||||
import '../../widgets/monitoring_shared.dart';
|
||||
|
||||
class WindSpeedAnnualSection extends StatelessWidget {
|
||||
final VoidCallback onExportAnnual;
|
||||
|
||||
const WindSpeedAnnualSection({
|
||||
super.key,
|
||||
required this.onExportAnnual,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Statistik Tahunan Kecepatan Angin",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Row(
|
||||
children: const [
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
title: "Rata-rata Tahunan",
|
||||
value: "7.1 km/h",
|
||||
color: Colors.green,
|
||||
icon: Icons.air,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
title: "Kecepatan Max",
|
||||
value: "16.5 km/h",
|
||||
color: Colors.red,
|
||||
icon: Icons.trending_up,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"Grafik Tahunan - Rata-rata & Kecepatan Maksimal",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Card(
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
height: 320,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 2,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: Colors.grey.shade300,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: const [
|
||||
FlSpot(0, 5.5),
|
||||
FlSpot(1, 6.2),
|
||||
FlSpot(2, 6.8),
|
||||
FlSpot(3, 7.5),
|
||||
FlSpot(4, 7.8),
|
||||
FlSpot(5, 8.0),
|
||||
FlSpot(6, 8.2),
|
||||
FlSpot(7, 8.0),
|
||||
FlSpot(8, 7.2),
|
||||
FlSpot(9, 6.5),
|
||||
FlSpot(10, 5.8),
|
||||
FlSpot(11, 5.2),
|
||||
],
|
||||
isCurved: true,
|
||||
barWidth: 2.5,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: Colors.green,
|
||||
strokeWidth: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
color: Colors.green,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
LineChartBarData(
|
||||
spots: const [
|
||||
FlSpot(0, 12.0),
|
||||
FlSpot(1, 13.5),
|
||||
FlSpot(2, 14.0),
|
||||
FlSpot(3, 15.5),
|
||||
FlSpot(4, 16.0),
|
||||
FlSpot(5, 16.2),
|
||||
FlSpot(6, 16.5),
|
||||
FlSpot(7, 16.0),
|
||||
FlSpot(8, 15.0),
|
||||
FlSpot(9, 14.0),
|
||||
FlSpot(10, 12.5),
|
||||
FlSpot(11, 11.0),
|
||||
],
|
||||
isCurved: true,
|
||||
barWidth: 2.5,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: Colors.red,
|
||||
strokeWidth: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
"Rata-rata (km/h)",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
"Kecepatan Max (km/h)",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onExportAnnual,
|
||||
icon: const Icon(Icons.file_download),
|
||||
label: const Text("Download Excel Tahunan"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class WindSpeedExplanation extends StatelessWidget {
|
||||
const WindSpeedExplanation({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
"Penjelasan Kecepatan Angin",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
"Kecepatan angin adalah laju gerakan massa udara yang mengalir secara horizontal. Data kecepatan angin penting untuk prediksi cuaca, manajemen rawan angin, perencanaan energi terbarukan (angin), serta membantu dalam pengeringan hasil pertanian. Pengukuran dilakukan dalam satuan km/h.",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.6,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
|
||||
import '../../widgets/monitoring_shared.dart';
|
||||
|
||||
class WindSpeedGraphBlock extends StatelessWidget {
|
||||
final String selectedPeriod;
|
||||
final List<double> dailySpeeds;
|
||||
final bool isOnline;
|
||||
final ValueChanged<String> onPeriodChanged;
|
||||
final ValueChanged<String> onExportPressed;
|
||||
|
||||
const WindSpeedGraphBlock({
|
||||
super.key,
|
||||
required this.selectedPeriod,
|
||||
required this.dailySpeeds,
|
||||
required this.isOnline,
|
||||
required this.onPeriodChanged,
|
||||
required this.onExportPressed,
|
||||
});
|
||||
|
||||
List<FlSpot> _getPeriodSpots() {
|
||||
switch (selectedPeriod) {
|
||||
case "Hari Ini":
|
||||
return List.generate(24, (i) {
|
||||
final value = dailySpeeds[i];
|
||||
return FlSpot(i.toDouble(), value);
|
||||
});
|
||||
case "Minggu Ini":
|
||||
return const [
|
||||
FlSpot(0, 6.5),
|
||||
FlSpot(1, 7.2),
|
||||
FlSpot(2, 7.8),
|
||||
FlSpot(3, 8.0),
|
||||
FlSpot(4, 7.5),
|
||||
FlSpot(5, 6.8),
|
||||
FlSpot(6, 5.5),
|
||||
];
|
||||
case "Bulan Ini":
|
||||
return const [
|
||||
FlSpot(0, 6.0),
|
||||
FlSpot(1, 6.5),
|
||||
FlSpot(2, 7.0),
|
||||
FlSpot(3, 7.5),
|
||||
FlSpot(4, 7.8),
|
||||
FlSpot(5, 8.0),
|
||||
FlSpot(6, 8.2),
|
||||
FlSpot(7, 8.0),
|
||||
FlSpot(8, 7.5),
|
||||
FlSpot(9, 7.0),
|
||||
FlSpot(10, 6.5),
|
||||
FlSpot(11, 6.0),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Status Kecepatan Angin",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isOnline ? Colors.green.shade100 : Colors.red.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
isOnline ? "ONLINE" : "OFFLINE",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isOnline ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children:
|
||||
["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text(period),
|
||||
selected: selectedPeriod == period,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
onPeriodChanged(period);
|
||||
}
|
||||
},
|
||||
selectedColor: Colors.green,
|
||||
labelStyle: TextStyle(
|
||||
color: selectedPeriod == period
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => onExportPressed(selectedPeriod),
|
||||
icon: const Icon(Icons.download, size: 16),
|
||||
label: const Text("Export"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 300,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 1,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: Colors.grey.shade300,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: getPeriodInterval(selectedPeriod),
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
getPeriodLabel(selectedPeriod, value),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 1,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
value.toInt().toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 28,
|
||||
),
|
||||
),
|
||||
topTitles:
|
||||
AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: _getPeriodSpots(),
|
||||
isCurved: true,
|
||||
barWidth: 2.5,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: Colors.green,
|
||||
strokeWidth: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
color: Colors.green,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class WindSpeedStatusBlock extends StatelessWidget {
|
||||
final double currentSpeed;
|
||||
final bool isOnline;
|
||||
final DateTime lastUpdateTime;
|
||||
|
||||
const WindSpeedStatusBlock({
|
||||
super.key,
|
||||
required this.currentSpeed,
|
||||
required this.isOnline,
|
||||
required this.lastUpdateTime,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formattedTime =
|
||||
DateFormat("dd MMM yyyy • HH:mm:ss").format(lastUpdateTime);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${currentSpeed.toStringAsFixed(2)} km/h",
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOnline ? Colors.green.shade100 : Colors.red.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
isOnline ? "ONLINE" : "OFFLINE",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isOnline ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Update: $formattedTime",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widgets/monitoring_shared.dart';
|
||||
import 'widgets/wind_speed_annual_section.dart';
|
||||
import 'widgets/wind_speed_explanation.dart';
|
||||
import 'widgets/wind_speed_graph_block.dart';
|
||||
import 'widgets/wind_speed_status_block.dart';
|
||||
|
||||
class WindSpeedMonitoringScreen extends StatefulWidget {
|
||||
const WindSpeedMonitoringScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WindSpeedMonitoringScreen> createState() =>
|
||||
_WindSpeedMonitoringScreenState();
|
||||
}
|
||||
|
||||
class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
|
||||
String _selectedPeriod = "Hari Ini";
|
||||
|
||||
// realtime database state
|
||||
final DatabaseReference _realtimeRef =
|
||||
FirebaseDatabase.instance.ref('anemometer/realtime');
|
||||
StreamSubscription<DatabaseEvent>? _realtimeSub;
|
||||
|
||||
// realtime values
|
||||
DateTime? _lastUpdateTime;
|
||||
double _currentSpeed = 0.0;
|
||||
final List<double> _dailySpeeds = List<double>.filled(24, 0.0);
|
||||
|
||||
// hourly aggregation helpers (PRO version)
|
||||
double _hourlyTotalSpeed = 0.0;
|
||||
int _hourlyCount = 0;
|
||||
int _currentHour = DateTime.now().hour;
|
||||
|
||||
bool get _isOnline {
|
||||
if (_lastUpdateTime == null) return false;
|
||||
final diff = DateTime.now().difference(_lastUpdateTime!);
|
||||
return diff.inSeconds <= 5;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_realtimeSub = _realtimeRef.onValue.listen(_onRealtimeData);
|
||||
}
|
||||
|
||||
void _onRealtimeData(DatabaseEvent event) {
|
||||
final data = event.snapshot.value;
|
||||
|
||||
if (data is Map) {
|
||||
final speed = (data['kecepatan'] ?? 0).toDouble();
|
||||
final timestamp = data['timestamp'] ?? 0;
|
||||
|
||||
final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
|
||||
setState(() {
|
||||
_currentSpeed = speed;
|
||||
_lastUpdateTime = waktu;
|
||||
|
||||
final incomingHour = waktu.hour;
|
||||
|
||||
if (incomingHour != _currentHour) {
|
||||
if (_hourlyCount > 0) {
|
||||
final avg = _hourlyTotalSpeed / _hourlyCount;
|
||||
_dailySpeeds[_currentHour] = avg;
|
||||
}
|
||||
|
||||
_hourlyTotalSpeed = 0.0;
|
||||
_hourlyCount = 0;
|
||||
_currentHour = incomingHour;
|
||||
|
||||
if (incomingHour == 0) {
|
||||
for (int i = 0; i < 24; i++) {
|
||||
_dailySpeeds[i] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_hourlyTotalSpeed += speed;
|
||||
_hourlyCount++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_realtimeSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleExportSelectedPeriod(String period) {
|
||||
switch (period) {
|
||||
case "Hari Ini":
|
||||
_exportDailyWindData();
|
||||
break;
|
||||
case "Minggu Ini":
|
||||
_exportWeeklyWindData();
|
||||
break;
|
||||
case "Bulan Ini":
|
||||
_exportMonthlyWindData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _exportDailyWindData() {
|
||||
final StringBuffer csvContent = StringBuffer();
|
||||
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - HARIAN (24 JAM)");
|
||||
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Jam,Kecepatan (km/h)");
|
||||
|
||||
final dailyData = [
|
||||
5.0,
|
||||
5.2,
|
||||
4.8,
|
||||
4.5,
|
||||
4.3,
|
||||
4.6,
|
||||
5.1,
|
||||
6.0,
|
||||
7.2,
|
||||
8.5,
|
||||
9.0,
|
||||
10.2,
|
||||
11.0,
|
||||
10.5,
|
||||
10.0,
|
||||
9.2,
|
||||
8.0,
|
||||
6.5,
|
||||
5.5,
|
||||
5.0,
|
||||
4.8,
|
||||
4.5,
|
||||
5.0,
|
||||
5.2
|
||||
];
|
||||
|
||||
for (int i = 0; i < dailyData.length; i++) {
|
||||
csvContent.writeln("$i:00,${dailyData[i]}");
|
||||
}
|
||||
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Statistik Harian");
|
||||
csvContent.writeln("Total: 165.8 km/h");
|
||||
csvContent.writeln("Rata-rata: 6.9 km/h");
|
||||
csvContent.writeln("Maksimal: 11.0 km/h");
|
||||
csvContent.writeln("Minimal: 4.3 km/h");
|
||||
|
||||
showExportPreview(
|
||||
context, csvContent.toString(), "Data Harian Kecepatan Angin");
|
||||
}
|
||||
|
||||
void _exportWeeklyWindData() {
|
||||
final StringBuffer csvContent = StringBuffer();
|
||||
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - MINGGUAN");
|
||||
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Hari,Rata-rata (km/h)");
|
||||
|
||||
final days = [
|
||||
'Senin',
|
||||
'Selasa',
|
||||
'Rabu',
|
||||
'Kamis',
|
||||
'Jumat',
|
||||
'Sabtu',
|
||||
'Minggu'
|
||||
];
|
||||
final weeklyData = [6.5, 7.2, 7.8, 8.0, 7.5, 6.8, 5.5];
|
||||
|
||||
for (int i = 0; i < days.length; i++) {
|
||||
csvContent.writeln("${days[i]},${weeklyData[i]}");
|
||||
}
|
||||
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Statistik Mingguan");
|
||||
csvContent.writeln("Total: 49.3 km/h");
|
||||
csvContent.writeln("Rata-rata: 7.0 km/h");
|
||||
csvContent.writeln("Maksimal: 8.0 km/h");
|
||||
csvContent.writeln("Minimal: 5.5 km/h");
|
||||
|
||||
showExportPreview(
|
||||
context, csvContent.toString(), "Data Mingguan Kecepatan Angin");
|
||||
}
|
||||
|
||||
void _exportMonthlyWindData() {
|
||||
final StringBuffer csvContent = StringBuffer();
|
||||
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - BULANAN");
|
||||
csvContent.writeln(
|
||||
"Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}");
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Tanggal,Rata-rata (km/h)");
|
||||
|
||||
for (int i = 1; i <= 28; i++) {
|
||||
final value = (6.0 + (i % 6) * 0.8).toStringAsFixed(1);
|
||||
csvContent.writeln("$i,$value");
|
||||
}
|
||||
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Statistik Bulanan");
|
||||
csvContent.writeln("Total: 180.5 km/h");
|
||||
csvContent.writeln("Rata-rata: 6.4 km/h");
|
||||
csvContent.writeln("Maksimal: 9.0 km/h");
|
||||
csvContent.writeln("Minimal: 4.0 km/h");
|
||||
|
||||
showExportPreview(
|
||||
context, csvContent.toString(), "Data Bulanan Kecepatan Angin");
|
||||
}
|
||||
|
||||
void _exportAnnualWindData() {
|
||||
final StringBuffer csvContent = StringBuffer();
|
||||
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - TAHUNAN");
|
||||
csvContent.writeln("Tahun: 2025");
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Bulan,Rata-rata (km/h),Kecepatan Max (km/h)");
|
||||
|
||||
final months = [
|
||||
'Januari',
|
||||
'Februari',
|
||||
'Maret',
|
||||
'April',
|
||||
'Mei',
|
||||
'Juni',
|
||||
'Juli',
|
||||
'Agustus',
|
||||
'September',
|
||||
'Oktober',
|
||||
'November',
|
||||
'Desember'
|
||||
];
|
||||
final averages = [
|
||||
5.5,
|
||||
6.2,
|
||||
6.8,
|
||||
7.5,
|
||||
7.8,
|
||||
8.0,
|
||||
8.2,
|
||||
8.0,
|
||||
7.2,
|
||||
6.5,
|
||||
5.8,
|
||||
5.2
|
||||
];
|
||||
final maxSpeed = [
|
||||
12.0,
|
||||
13.5,
|
||||
14.0,
|
||||
15.5,
|
||||
16.0,
|
||||
16.2,
|
||||
16.5,
|
||||
16.0,
|
||||
15.0,
|
||||
14.0,
|
||||
12.5,
|
||||
11.0
|
||||
];
|
||||
|
||||
for (int i = 0; i < months.length; i++) {
|
||||
csvContent.writeln("${months[i]},${averages[i]},${maxSpeed[i]}");
|
||||
}
|
||||
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("STATISTIK TAHUNAN");
|
||||
csvContent.writeln("Rata-rata Tahunan: 7.1 km/h");
|
||||
csvContent.writeln("Kecepatan Max Tertinggi: 16.5 km/h");
|
||||
csvContent.writeln("Kecepatan Min Terendah: 5.2 km/h");
|
||||
|
||||
showExportPreview(
|
||||
context, csvContent.toString(), "Data Tahunan Kecepatan Angin");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Monitoring Kecepatan Angin"),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Kecepatan Angin",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_lastUpdateTime != null)
|
||||
WindSpeedStatusBlock(
|
||||
currentSpeed: _currentSpeed,
|
||||
isOnline: _isOnline,
|
||||
lastUpdateTime: _lastUpdateTime!,
|
||||
),
|
||||
WindSpeedGraphBlock(
|
||||
selectedPeriod: _selectedPeriod,
|
||||
dailySpeeds: _dailySpeeds,
|
||||
isOnline: _isOnline,
|
||||
onPeriodChanged: (period) {
|
||||
setState(() {
|
||||
_selectedPeriod = period;
|
||||
});
|
||||
},
|
||||
onExportPressed: _handleExportSelectedPeriod,
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
const WindSpeedExplanation(),
|
||||
const SizedBox(height: 25),
|
||||
WindSpeedAnnualSection(
|
||||
onExportAnnual: _exportAnnualWindData,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,815 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'dart:async';
|
||||
import 'widgets/monitoring_shared.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// WIND SPEED MONITORING SCREEN
|
||||
class WindSpeedMonitoringScreen extends StatefulWidget {
|
||||
const WindSpeedMonitoringScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WindSpeedMonitoringScreen> createState() => _WindSpeedMonitoringScreenState();
|
||||
}
|
||||
|
||||
class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
|
||||
String _selectedPeriod = "Hari Ini";
|
||||
|
||||
// realtime database state
|
||||
final DatabaseReference _realtimeRef = FirebaseDatabase.instance.ref('anemometer/realtime');
|
||||
StreamSubscription<DatabaseEvent>? _realtimeSub;
|
||||
|
||||
// realtime values
|
||||
DateTime? _lastUpdateTime;
|
||||
double _currentSpeed = 0.0;
|
||||
final List<double> _dailySpeeds = List<double>.filled(24, 0.0);
|
||||
|
||||
// hourly aggregation helpers (PRO version)
|
||||
double _hourlyTotalSpeed = 0.0;
|
||||
int _hourlyCount = 0;
|
||||
int _currentHour = DateTime.now().hour;
|
||||
|
||||
bool get _isOnline {
|
||||
if (_lastUpdateTime == null) return false;
|
||||
final diff = DateTime.now().difference(_lastUpdateTime!);
|
||||
return diff.inSeconds <= 5;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_realtimeSub = _realtimeRef.onValue.listen(_onRealtimeData);
|
||||
}
|
||||
|
||||
void _onRealtimeData(DatabaseEvent event) {
|
||||
final data = event.snapshot.value;
|
||||
|
||||
if (data is Map) {
|
||||
final speed = (data['kecepatan'] ?? 0).toDouble();
|
||||
final timestamp = data['timestamp'] ?? 0;
|
||||
|
||||
final waktu = DateTime.fromMillisecondsSinceEpoch(
|
||||
timestamp * 1000,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_currentSpeed = speed;
|
||||
_lastUpdateTime = waktu;
|
||||
|
||||
final incomingHour = waktu.hour;
|
||||
|
||||
// Jika jam berubah → simpan rata-rata jam sebelumnya
|
||||
if (incomingHour != _currentHour) {
|
||||
|
||||
if (_hourlyCount > 0) {
|
||||
final avg = _hourlyTotalSpeed / _hourlyCount;
|
||||
|
||||
// Simpan ke index sesuai jam sebelumnya
|
||||
_dailySpeeds[_currentHour] = avg;
|
||||
}
|
||||
|
||||
// Reset agregasi
|
||||
_hourlyTotalSpeed = 0.0;
|
||||
_hourlyCount = 0;
|
||||
|
||||
_currentHour = incomingHour;
|
||||
|
||||
// Reset data saat masuk hari baru (00:00)
|
||||
if (incomingHour == 0) {
|
||||
for (int i = 0; i < 24; i++) {
|
||||
_dailySpeeds[i] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tambah akumulasi
|
||||
_hourlyTotalSpeed += speed;
|
||||
_hourlyCount++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_realtimeSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Monitoring Kecepatan Angin"),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Kecepatan Angin",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_lastUpdateTime != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_currentSpeed.toStringAsFixed(2)} km/h",
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _isOnline
|
||||
? Colors.green.shade100
|
||||
: Colors.red.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
_isOnline ? "ONLINE" : "OFFLINE",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _isOnline ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Builder(
|
||||
builder: (_) {
|
||||
final formattedTime = DateFormat(
|
||||
"dd MMM yyyy • HH:mm:ss",
|
||||
).format(_lastUpdateTime!);
|
||||
return Text(
|
||||
"Update: $formattedTime",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
/// GRAFIK MONITORING
|
||||
_buildMonitoringGraphBlock(),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
/// PENJELASAN
|
||||
_buildWindSpeedExplanation(),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
/// STATISTIK TAHUNAN
|
||||
_buildAnnualWindGraphSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// EXPORT FUNCTIONS
|
||||
void _exportDailyWindData() {
|
||||
final StringBuffer csvContent = StringBuffer();
|
||||
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - HARIAN (24 JAM)");
|
||||
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Jam,Kecepatan (km/h)");
|
||||
|
||||
final dailyData = [
|
||||
5.0, 5.2, 4.8, 4.5, 4.3, 4.6, 5.1, 6.0, 7.2, 8.5,
|
||||
9.0, 10.2, 11.0, 10.5, 10.0, 9.2, 8.0, 6.5, 5.5, 5.0, 4.8, 4.5, 5.0, 5.2
|
||||
];
|
||||
for (int i = 0; i < dailyData.length; i++) {
|
||||
csvContent.writeln("$i:00,${dailyData[i]}");
|
||||
}
|
||||
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Statistik Harian");
|
||||
csvContent.writeln("Total: 165.8 km/h");
|
||||
csvContent.writeln("Rata-rata: 6.9 km/h");
|
||||
csvContent.writeln("Maksimal: 11.0 km/h");
|
||||
csvContent.writeln("Minimal: 4.3 km/h");
|
||||
|
||||
showExportPreview(context, csvContent.toString(), "Data Harian Kecepatan Angin");
|
||||
}
|
||||
|
||||
void _exportWeeklyWindData() {
|
||||
final StringBuffer csvContent = StringBuffer();
|
||||
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - MINGGUAN");
|
||||
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Hari,Rata-rata (km/h)");
|
||||
|
||||
final days = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
|
||||
final weeklyData = [6.5, 7.2, 7.8, 8.0, 7.5, 6.8, 5.5];
|
||||
|
||||
for (int i = 0; i < days.length; i++) {
|
||||
csvContent.writeln("${days[i]},${weeklyData[i]}");
|
||||
}
|
||||
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Statistik Mingguan");
|
||||
csvContent.writeln("Total: 49.3 km/h");
|
||||
csvContent.writeln("Rata-rata: 7.0 km/h");
|
||||
csvContent.writeln("Maksimal: 8.0 km/h");
|
||||
csvContent.writeln("Minimal: 5.5 km/h");
|
||||
|
||||
showExportPreview(context, csvContent.toString(), "Data Mingguan Kecepatan Angin");
|
||||
}
|
||||
|
||||
void _exportMonthlyWindData() {
|
||||
final StringBuffer csvContent = StringBuffer();
|
||||
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - BULANAN");
|
||||
csvContent.writeln("Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}");
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Tanggal,Rata-rata (km/h)");
|
||||
|
||||
for (int i = 1; i <= 28; i++) {
|
||||
final value = (6.0 + (i % 6) * 0.8).toStringAsFixed(1);
|
||||
csvContent.writeln("$i,$value");
|
||||
}
|
||||
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Statistik Bulanan");
|
||||
csvContent.writeln("Total: 180.5 km/h");
|
||||
csvContent.writeln("Rata-rata: 6.4 km/h");
|
||||
csvContent.writeln("Maksimal: 9.0 km/h");
|
||||
csvContent.writeln("Minimal: 4.0 km/h");
|
||||
|
||||
showExportPreview(context, csvContent.toString(), "Data Bulanan Kecepatan Angin");
|
||||
}
|
||||
|
||||
void _exportAnnualWindData() {
|
||||
final StringBuffer csvContent = StringBuffer();
|
||||
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - TAHUNAN");
|
||||
csvContent.writeln("Tahun: 2025");
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("Bulan,Rata-rata (km/h),Kecepatan Max (km/h)");
|
||||
|
||||
final months = [
|
||||
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
|
||||
];
|
||||
final averages = [5.5, 6.2, 6.8, 7.5, 7.8, 8.0, 8.2, 8.0, 7.2, 6.5, 5.8, 5.2];
|
||||
final maxSpeed = [12.0, 13.5, 14.0, 15.5, 16.0, 16.2, 16.5, 16.0, 15.0, 14.0, 12.5, 11.0];
|
||||
|
||||
for (int i = 0; i < months.length; i++) {
|
||||
csvContent.writeln("${months[i]},${averages[i]},${maxSpeed[i]}");
|
||||
}
|
||||
|
||||
csvContent.writeln("");
|
||||
csvContent.writeln("STATISTIK TAHUNAN");
|
||||
csvContent.writeln("Rata-rata Tahunan: 7.1 km/h");
|
||||
csvContent.writeln("Kecepatan Max Tertinggi: 16.5 km/h");
|
||||
csvContent.writeln("Kecepatan Min Terendah: 5.2 km/h");
|
||||
|
||||
showExportPreview(context, csvContent.toString(), "Data Tahunan Kecepatan Angin");
|
||||
}
|
||||
|
||||
/// DATA GENERATORS
|
||||
List<FlSpot> _getWindSpeedPeriodData() {
|
||||
switch (_selectedPeriod) {
|
||||
case "Hari Ini":
|
||||
// use live-updated hourly speeds if available
|
||||
return List.generate(24, (i) {
|
||||
final value = _dailySpeeds[i];
|
||||
return FlSpot(i.toDouble(), value);
|
||||
});
|
||||
case "Minggu Ini":
|
||||
return [
|
||||
const FlSpot(0, 6.5),
|
||||
const FlSpot(1, 7.2),
|
||||
const FlSpot(2, 7.8),
|
||||
const FlSpot(3, 8.0),
|
||||
const FlSpot(4, 7.5),
|
||||
const FlSpot(5, 6.8),
|
||||
const FlSpot(6, 5.5),
|
||||
];
|
||||
case "Bulan Ini":
|
||||
return [
|
||||
const FlSpot(0, 6.0),
|
||||
const FlSpot(1, 6.5),
|
||||
const FlSpot(2, 7.0),
|
||||
const FlSpot(3, 7.5),
|
||||
const FlSpot(4, 7.8),
|
||||
const FlSpot(5, 8.0),
|
||||
const FlSpot(6, 8.2),
|
||||
const FlSpot(7, 8.0),
|
||||
const FlSpot(8, 7.5),
|
||||
const FlSpot(9, 7.0),
|
||||
const FlSpot(10, 6.5),
|
||||
const FlSpot(11, 6.0),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// UI BUILDERS
|
||||
Widget _buildMonitoringGraphBlock() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Status Kecepatan Angin",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _isOnline ? Colors.green.shade100 : Colors.red.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
_isOnline ? "ONLINE" : "OFFLINE",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _isOnline ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
/// PERIOD SELECTOR + EXPORT
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: ["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text(period),
|
||||
selected: _selectedPeriod == period,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_selectedPeriod = period;
|
||||
});
|
||||
},
|
||||
selectedColor: Colors.green,
|
||||
labelStyle: TextStyle(
|
||||
color: _selectedPeriod == period
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (_selectedPeriod == "Hari Ini") {
|
||||
_exportDailyWindData();
|
||||
} else if (_selectedPeriod == "Minggu Ini") {
|
||||
_exportWeeklyWindData();
|
||||
} else if (_selectedPeriod == "Bulan Ini") {
|
||||
_exportMonthlyWindData();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.download, size: 16),
|
||||
label: const Text("Export"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
/// GRAPH
|
||||
SizedBox(
|
||||
height: 300,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 1,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: Colors.grey.shade300,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: getPeriodInterval(_selectedPeriod),
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
getPeriodLabel(_selectedPeriod, value),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 1,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
value.toInt().toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 28,
|
||||
),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: _getWindSpeedPeriodData(), // uses _dailySpeeds now
|
||||
isCurved: true,
|
||||
barWidth: 2.5,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: Colors.green,
|
||||
strokeWidth: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
color: Colors.green,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWindSpeedExplanation() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Penjelasan Kecepatan Angin",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"Kecepatan angin adalah laju gerakan massa udara yang mengalir secara horizontal. Data kecepatan angin penting untuk prediksi cuaca, manajemen rawan angin, perencanaan energi terbarukan (angin), serta membantu dalam pengeringan hasil pertanian. Pengukuran dilakukan dalam satuan km/h.",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.6,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnnualWindGraphSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Statistik Tahunan Kecepatan Angin",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
/// STATS
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
title: "Rata-rata Tahunan",
|
||||
value: "7.1 km/h",
|
||||
color: Colors.green,
|
||||
icon: Icons.air,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
title: "Kecepatan Max",
|
||||
value: "16.5 km/h",
|
||||
color: Colors.red,
|
||||
icon: Icons.trending_up,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
/// CHART
|
||||
const Text(
|
||||
"Grafik Tahunan - Rata-rata & Kecepatan Maksimal",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Card(
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
height: 320,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 2,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: Colors.grey.shade300,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final months = [
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
|
||||
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
|
||||
];
|
||||
return Text(
|
||||
months[value.toInt()],
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 30,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
'${value.toInt()} km/h',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 50,
|
||||
),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
/// Rata-rata
|
||||
LineChartBarData(
|
||||
spots: const [
|
||||
FlSpot(0, 5.5),
|
||||
FlSpot(1, 6.2),
|
||||
FlSpot(2, 6.8),
|
||||
FlSpot(3, 7.5),
|
||||
FlSpot(4, 7.8),
|
||||
FlSpot(5, 8.0),
|
||||
FlSpot(6, 8.2),
|
||||
FlSpot(7, 8.0),
|
||||
FlSpot(8, 7.2),
|
||||
FlSpot(9, 6.5),
|
||||
FlSpot(10, 5.8),
|
||||
FlSpot(11, 5.2),
|
||||
],
|
||||
isCurved: true,
|
||||
barWidth: 2.5,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: Colors.green,
|
||||
strokeWidth: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
color: Colors.green,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
/// Max Speed
|
||||
LineChartBarData(
|
||||
spots: const [
|
||||
FlSpot(0, 12.0),
|
||||
FlSpot(1, 13.5),
|
||||
FlSpot(2, 14.0),
|
||||
FlSpot(3, 15.5),
|
||||
FlSpot(4, 16.0),
|
||||
FlSpot(5, 16.2),
|
||||
FlSpot(6, 16.5),
|
||||
FlSpot(7, 16.0),
|
||||
FlSpot(8, 15.0),
|
||||
FlSpot(9, 14.0),
|
||||
FlSpot(10, 12.5),
|
||||
FlSpot(11, 11.0),
|
||||
],
|
||||
isCurved: true,
|
||||
barWidth: 2.5,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: Colors.red,
|
||||
strokeWidth: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
/// LEGEND
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
"Rata-rata (km/h)",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
"Kecepatan Max (km/h)",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _exportAnnualWindData(),
|
||||
icon: const Icon(Icons.file_download),
|
||||
label: const Text("Download Excel Tahunan"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue