jossss
This commit is contained in:
parent
015ff0da93
commit
60b2047eae
|
|
@ -1,780 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
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/wind_speed.dart';
|
|
||||||
import '../../monitoring/air_quality_monitoring_screen.dart';
|
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<HomeScreen> createState() => _HomeScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// simple model to represent one activity log item
|
|
||||||
class ActivityItem {
|
|
||||||
final String type; // evaporasi, angin, udara, etc
|
|
||||||
final String message;
|
|
||||||
final DateTime time;
|
|
||||||
|
|
||||||
ActivityItem({
|
|
||||||
required this.type,
|
|
||||||
required this.message,
|
|
||||||
required this.time,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
|
||||||
// Firebase realtime database references
|
|
||||||
final DatabaseReference _evaporasiRef =
|
|
||||||
FirebaseDatabase.instance.ref('kelompok1/evaporasi');
|
|
||||||
final DatabaseReference _windSpeedRef =
|
|
||||||
FirebaseDatabase.instance.ref('anemometer/realtime');
|
|
||||||
final DatabaseReference _airQualityRef =
|
|
||||||
FirebaseDatabase.instance.ref('air_quality/realtime');
|
|
||||||
|
|
||||||
// Stream subscriptions
|
|
||||||
StreamSubscription<DatabaseEvent>? _evaporasiSub;
|
|
||||||
StreamSubscription<DatabaseEvent>? _windSpeedSub;
|
|
||||||
StreamSubscription<DatabaseEvent>? _airQualitySub;
|
|
||||||
|
|
||||||
// Realtime values
|
|
||||||
double _currentEvaporasi = 0.0;
|
|
||||||
double _currentWindSpeed = 0.0;
|
|
||||||
double _currentAirQuality = 0.0;
|
|
||||||
|
|
||||||
DateTime? _evaporasiUpdateTime;
|
|
||||||
DateTime? _windSpeedUpdateTime;
|
|
||||||
DateTime? _airQualityUpdateTime;
|
|
||||||
|
|
||||||
// Hourly data for charts (24 hours)
|
|
||||||
final List<double> _evaporasiData = List<double>.filled(24, 0.0);
|
|
||||||
final List<double> _windSpeedData = List<double>.filled(24, 0.0);
|
|
||||||
final List<double> _airQualityData = List<double>.filled(24, 0.0);
|
|
||||||
|
|
||||||
// activity log
|
|
||||||
final List<ActivityItem> _activities = [];
|
|
||||||
|
|
||||||
// Hourly aggregation helpers
|
|
||||||
double _evaporasiHourlyTotal = 0.0;
|
|
||||||
int _evaporasiHourlyCount = 0;
|
|
||||||
|
|
||||||
double _windSpeedHourlyTotal = 0.0;
|
|
||||||
int _windSpeedHourlyCount = 0;
|
|
||||||
|
|
||||||
double _airQualityHourlyTotal = 0.0;
|
|
||||||
int _airQualityHourlyCount = 0;
|
|
||||||
|
|
||||||
int _currentHour = DateTime.now().hour;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_evaporasiSub = _evaporasiRef.onValue.listen(_onEvaporasiData);
|
|
||||||
_windSpeedSub = _windSpeedRef.onValue.listen(_onWindSpeedData);
|
|
||||||
_airQualitySub = _airQualityRef.onValue.listen(_onAirQualityData);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onEvaporasiData(DatabaseEvent event) {
|
|
||||||
final data = event.snapshot.value;
|
|
||||||
if (data is Map) {
|
|
||||||
final value = double.tryParse(
|
|
||||||
(data['evaporasi'] ?? data['nilai'] ?? 0).toString()) ??
|
|
||||||
0.0;
|
|
||||||
DateTime waktu;
|
|
||||||
|
|
||||||
if (data.containsKey('timestamp')) {
|
|
||||||
final timestamp =
|
|
||||||
int.tryParse(data['timestamp']?.toString() ?? '0') ?? 0;
|
|
||||||
waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
||||||
} else {
|
|
||||||
final hour = int.tryParse(data['jam']?.toString() ?? '0') ?? 0;
|
|
||||||
final minute = int.tryParse(data['menit']?.toString() ?? '0') ?? 0;
|
|
||||||
final now = DateTime.now();
|
|
||||||
waktu = DateTime(now.year, now.month, now.day, hour, minute);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_currentEvaporasi = value;
|
|
||||||
_evaporasiUpdateTime = waktu;
|
|
||||||
|
|
||||||
// add to activity log
|
|
||||||
_activities.insert(
|
|
||||||
0,
|
|
||||||
ActivityItem(
|
|
||||||
type: "evaporasi",
|
|
||||||
message: "Evaporasi tercatat ${value.toStringAsFixed(1)} mm",
|
|
||||||
time: waktu,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (_activities.length > 50) _activities.removeLast();
|
|
||||||
|
|
||||||
final incomingHour = waktu.hour;
|
|
||||||
|
|
||||||
if (incomingHour != _currentHour) {
|
|
||||||
if (_evaporasiHourlyCount > 0) {
|
|
||||||
final avg = _evaporasiHourlyTotal / _evaporasiHourlyCount;
|
|
||||||
_evaporasiData[_currentHour] = avg;
|
|
||||||
}
|
|
||||||
|
|
||||||
_evaporasiHourlyTotal = 0.0;
|
|
||||||
_evaporasiHourlyCount = 0;
|
|
||||||
|
|
||||||
_currentHour = incomingHour;
|
|
||||||
|
|
||||||
if (incomingHour == 0) {
|
|
||||||
for (int i = 0; i < 24; i++) {
|
|
||||||
_evaporasiData[i] = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_evaporasiHourlyTotal += value;
|
|
||||||
_evaporasiHourlyCount++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onWindSpeedData(DatabaseEvent event) {
|
|
||||||
final data = event.snapshot.value;
|
|
||||||
if (data is Map) {
|
|
||||||
final value = (data['kecepatan'] ?? 0).toDouble();
|
|
||||||
final timestamp = data['timestamp'] ?? 0;
|
|
||||||
final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_currentWindSpeed = value;
|
|
||||||
_windSpeedUpdateTime = waktu;
|
|
||||||
|
|
||||||
// add to activity log
|
|
||||||
_activities.insert(
|
|
||||||
0,
|
|
||||||
ActivityItem(
|
|
||||||
type: "angin",
|
|
||||||
message:
|
|
||||||
"Kecepatan angin update menjadi ${value.toStringAsFixed(1)} km/h",
|
|
||||||
time: waktu,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (_activities.length > 50) _activities.removeLast();
|
|
||||||
|
|
||||||
final incomingHour = waktu.hour;
|
|
||||||
|
|
||||||
if (incomingHour != _currentHour) {
|
|
||||||
if (_windSpeedHourlyCount > 0) {
|
|
||||||
final avg = _windSpeedHourlyTotal / _windSpeedHourlyCount;
|
|
||||||
_windSpeedData[_currentHour] = avg;
|
|
||||||
}
|
|
||||||
|
|
||||||
_windSpeedHourlyTotal = 0.0;
|
|
||||||
_windSpeedHourlyCount = 0;
|
|
||||||
|
|
||||||
_currentHour = incomingHour;
|
|
||||||
|
|
||||||
if (incomingHour == 0) {
|
|
||||||
for (int i = 0; i < 24; i++) {
|
|
||||||
_windSpeedData[i] = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_windSpeedHourlyTotal += value;
|
|
||||||
_windSpeedHourlyCount++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAirQualityData(DatabaseEvent event) {
|
|
||||||
final data = event.snapshot.value;
|
|
||||||
if (data is Map) {
|
|
||||||
final value = (data['aqi'] ?? 0).toDouble();
|
|
||||||
final timestamp = data['timestamp'] ?? 0;
|
|
||||||
final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_currentAirQuality = value;
|
|
||||||
_airQualityUpdateTime = waktu;
|
|
||||||
|
|
||||||
// add to activity log
|
|
||||||
_activities.insert(
|
|
||||||
0,
|
|
||||||
ActivityItem(
|
|
||||||
type: "udara",
|
|
||||||
message: "AQI berubah menjadi ${value.toStringAsFixed(0)}",
|
|
||||||
time: waktu,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (_activities.length > 50) _activities.removeLast();
|
|
||||||
|
|
||||||
final incomingHour = waktu.hour;
|
|
||||||
|
|
||||||
if (incomingHour != _currentHour) {
|
|
||||||
if (_airQualityHourlyCount > 0) {
|
|
||||||
final avg = _airQualityHourlyTotal / _airQualityHourlyCount;
|
|
||||||
_airQualityData[_currentHour] = avg;
|
|
||||||
}
|
|
||||||
|
|
||||||
_airQualityHourlyTotal = 0.0;
|
|
||||||
_airQualityHourlyCount = 0;
|
|
||||||
|
|
||||||
_currentHour = incomingHour;
|
|
||||||
|
|
||||||
if (incomingHour == 0) {
|
|
||||||
for (int i = 0; i < 24; i++) {
|
|
||||||
_airQualityData[i] = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_airQualityHourlyTotal += value;
|
|
||||||
_airQualityHourlyCount++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_evaporasiSub?.cancel();
|
|
||||||
_windSpeedSub?.cancel();
|
|
||||||
_airQualitySub?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold();
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text("Monitoring Klimatologi"),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
|
|
||||||
/// 🔹 SIDEBAR (MENU 3 GARIS DI KIRI)
|
|
||||||
drawer: Drawer(
|
|
||||||
child: SafeArea(
|
|
||||||
child: ListView(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: [
|
|
||||||
// 🔹 HEADER DENGAN AKUN INFO (dynamic dari AuthenticationBloc)
|
|
||||||
BlocBuilder<AuthenticationBloc, AuthenticationState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
final user = state.user;
|
|
||||||
final displayName = (user != null && user.name.isNotEmpty)
|
|
||||||
? user.name
|
|
||||||
: 'Pengguna';
|
|
||||||
final email = (user != null && user.email.isNotEmpty)
|
|
||||||
? user.email
|
|
||||||
: 'Tidak ada email';
|
|
||||||
final initials = (displayName.isNotEmpty)
|
|
||||||
? displayName
|
|
||||||
.trim()
|
|
||||||
.split(' ')
|
|
||||||
.map((e) => e.isNotEmpty ? e[0] : '')
|
|
||||||
.take(2)
|
|
||||||
.join()
|
|
||||||
: 'U';
|
|
||||||
|
|
||||||
return UserAccountsDrawerHeader(
|
|
||||||
accountName: Text(
|
|
||||||
displayName,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
accountEmail: Text(email),
|
|
||||||
currentAccountPicture: CircleAvatar(
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
child: Text(
|
|
||||||
initials.toUpperCase(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// 🔹 DASHBOARD MENU
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.dashboard),
|
|
||||||
title: const Text("Dashboard"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
const Divider(),
|
|
||||||
|
|
||||||
// 🔹 MONITORING MENU
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: Text(
|
|
||||||
"Monitoring",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
_buildDrawerItem(
|
|
||||||
context,
|
|
||||||
"Evaporasi",
|
|
||||||
Icons.opacity,
|
|
||||||
),
|
|
||||||
_buildDrawerItem(
|
|
||||||
context,
|
|
||||||
"Kecepatan Angin",
|
|
||||||
Icons.air,
|
|
||||||
),
|
|
||||||
_buildDrawerItem(
|
|
||||||
context,
|
|
||||||
"Kualitas Udara",
|
|
||||||
Icons.cloud,
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.logout),
|
|
||||||
title: const Text('Logout'),
|
|
||||||
onTap: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
// trigger logout via bloc
|
|
||||||
final bloc = context.read<AuthenticationBloc>();
|
|
||||||
bloc.add(const AuthenticationLogoutRequested());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
/// 🔹 DASHBOARD UTAMA - 3 MONITORING
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
BlocBuilder<AuthenticationBloc, AuthenticationState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.user?.name.isNotEmpty == true
|
|
||||||
? state.user!.name
|
|
||||||
: 'Pengguna';
|
|
||||||
return Text(
|
|
||||||
'Selamat Datang $name',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
const Text(
|
|
||||||
"Recent Activity",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Expanded(
|
|
||||||
child: _activities.isEmpty
|
|
||||||
? const Center(child: Text("Belum ada aktivitas"))
|
|
||||||
: ListView.builder(
|
|
||||||
itemCount: _activities.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item = _activities[index];
|
|
||||||
return _buildActivityTile(item);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DATA BLOCK - PREVIEW DATA HARI INI
|
|
||||||
Widget _buildDataBlock(
|
|
||||||
String title,
|
|
||||||
String value,
|
|
||||||
String unit,
|
|
||||||
Color color,
|
|
||||||
IconData icon,
|
|
||||||
) {
|
|
||||||
return Expanded(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: color, width: 2),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: color, size: 28),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
||||||
textBaseline: TextBaseline.alphabetic,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
unit,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DRAWER ITEM
|
|
||||||
Widget _buildDrawerItem(
|
|
||||||
BuildContext context,
|
|
||||||
String title,
|
|
||||||
IconData icon,
|
|
||||||
) {
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(icon),
|
|
||||||
title: Text(title),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
Widget screen;
|
|
||||||
if (title == "Evaporasi") {
|
|
||||||
screen = const EvaporasiMonitoringScreen();
|
|
||||||
} else if (title == "Kecepatan Angin") {
|
|
||||||
screen = const WindSpeedMonitoringScreen();
|
|
||||||
} else if (title == "Kualitas Udara") {
|
|
||||||
screen = const AirQualityMonitoringScreen();
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => screen),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DASHBOARD CARD - PREVIEW GRAFIK 24 JAM
|
|
||||||
Widget _buildDashboardCard(BuildContext context, String title) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Widget screen;
|
|
||||||
if (title == "Evaporasi") {
|
|
||||||
screen = const EvaporasiMonitoringScreen();
|
|
||||||
} else if (title == "Kecepatan Angin") {
|
|
||||||
screen = const WindSpeedMonitoringScreen();
|
|
||||||
} else if (title == "Kualitas Udara") {
|
|
||||||
screen = const AirQualityMonitoringScreen();
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => screen),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: 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: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.green.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
"24 Jam",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SizedBox(
|
|
||||||
height: 200,
|
|
||||||
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: 3,
|
|
||||||
getTitlesWidget: (value, meta) {
|
|
||||||
return Text(
|
|
||||||
'${value.toInt()}h',
|
|
||||||
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: _getMonitoringData(title),
|
|
||||||
isCurved: true,
|
|
||||||
barWidth: 2.5,
|
|
||||||
dotData: FlDotData(
|
|
||||||
show: true,
|
|
||||||
getDotPainter: (spot, percent, barData, index) {
|
|
||||||
return FlDotCirclePainter(
|
|
||||||
radius: 4,
|
|
||||||
color: _getColorByTitle(title),
|
|
||||||
strokeWidth: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
color: _getColorByTitle(title),
|
|
||||||
belowBarData: BarAreaData(
|
|
||||||
show: true,
|
|
||||||
color: _getColorByTitle(title).withOpacity(0.1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Rata-rata: ${_getAverageData(title)}",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Tap untuk detail",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GET MONITORING DATA FOR 24 HOURS
|
|
||||||
List<FlSpot> _getMonitoringData(String title) {
|
|
||||||
late final List<double> data;
|
|
||||||
|
|
||||||
switch (title) {
|
|
||||||
case "Evaporasi":
|
|
||||||
data = _evaporasiData;
|
|
||||||
break;
|
|
||||||
case "Kecepatan Angin":
|
|
||||||
data = _windSpeedData;
|
|
||||||
break;
|
|
||||||
case "Kualitas Udara":
|
|
||||||
data = _airQualityData;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
data = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return List.generate(
|
|
||||||
data.length,
|
|
||||||
(index) => FlSpot(index.toDouble(), data[index]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GET AVERAGE VALUE
|
|
||||||
String _getAverageData(String title) {
|
|
||||||
late final double average;
|
|
||||||
late final String unit;
|
|
||||||
|
|
||||||
switch (title) {
|
|
||||||
case "Evaporasi":
|
|
||||||
average = _evaporasiData.isNotEmpty
|
|
||||||
? _evaporasiData.reduce((a, b) => a + b) / _evaporasiData.length
|
|
||||||
: 0.0;
|
|
||||||
unit = "mm";
|
|
||||||
break;
|
|
||||||
case "Kecepatan Angin":
|
|
||||||
average = _windSpeedData.isNotEmpty
|
|
||||||
? _windSpeedData.reduce((a, b) => a + b) / _windSpeedData.length
|
|
||||||
: 0.0;
|
|
||||||
unit = "km/h";
|
|
||||||
break;
|
|
||||||
case "Kualitas Udara":
|
|
||||||
average = _airQualityData.isNotEmpty
|
|
||||||
? _airQualityData.reduce((a, b) => a + b) / _airQualityData.length
|
|
||||||
: 0.0;
|
|
||||||
unit = "AQI";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "${average.toStringAsFixed(1)} $unit";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GET COLOR BY TITLE
|
|
||||||
Color _getColorByTitle(String title) {
|
|
||||||
switch (title) {
|
|
||||||
case "Evaporasi":
|
|
||||||
return Colors.blue;
|
|
||||||
case "Kecepatan Angin":
|
|
||||||
return Colors.green;
|
|
||||||
case "Kualitas Udara":
|
|
||||||
return Colors.orange;
|
|
||||||
default:
|
|
||||||
return Colors.grey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ACTIVITY TILE
|
|
||||||
Widget _buildActivityTile(ActivityItem item) {
|
|
||||||
IconData icon;
|
|
||||||
Color color;
|
|
||||||
|
|
||||||
switch (item.type) {
|
|
||||||
case "evaporasi":
|
|
||||||
icon = Icons.opacity;
|
|
||||||
color = Colors.blue;
|
|
||||||
break;
|
|
||||||
case "angin":
|
|
||||||
icon = Icons.air;
|
|
||||||
color = Colors.green;
|
|
||||||
break;
|
|
||||||
case "udara":
|
|
||||||
icon = Icons.cloud;
|
|
||||||
color = Colors.orange;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
icon = Icons.info;
|
|
||||||
color = Colors.grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: color.withOpacity(0.2),
|
|
||||||
child: Icon(icon, color: color),
|
|
||||||
),
|
|
||||||
title: Text(item.message),
|
|
||||||
subtitle: Text(
|
|
||||||
"${item.time.hour}:${item.time.minute.toString().padLeft(2, '0')}",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,44 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
|
|
||||||
class WindSpeedBloc {
|
part 'wind_speed_event.dart';
|
||||||
final MonitoringRepository _repo;
|
part 'wind_speed_state.dart';
|
||||||
|
|
||||||
WindSpeedBloc({required MonitoringRepository repo}) : _repo = repo;
|
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
|
final MonitoringRepository _repository;
|
||||||
|
StreamSubscription? _subscription;
|
||||||
|
|
||||||
void startListening() {
|
WindSpeedBloc({required MonitoringRepository repository})
|
||||||
// 3. Sekarang kamu bisa akses _repo di dalam lingkup class ini
|
: _repository = repository,
|
||||||
_repo.getSensorStream('anemometer/realtime').listen((event) {
|
super(const WindSpeedState()) {
|
||||||
// Olah datanya di sini
|
// Handler saat aplikasi minta mulai monitoring
|
||||||
print(event.snapshot.value);
|
on<WatchWindSpeedStarted>((event, emit) {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_subscription = _repository
|
||||||
|
.getSensorStream(
|
||||||
|
'anemometer/realtime', (json) => MyWindSpeed.fromJson(json))
|
||||||
|
.listen((data) => add(_WindSpeedUpdated(data)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handler saat ada data baru masuk (Pindahan logika dari Screen)
|
||||||
|
on<_WindSpeedUpdated>((event, emit) {
|
||||||
|
// Di sini kamu bisa tambahkan logika hitung rata-rata/grafik harian
|
||||||
|
emit(state.copyWith(
|
||||||
|
currentSpeed: event.data.speed,
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
on<WindSpeedPeriodChanged>((event, emit) {
|
||||||
|
emit(state.copyWith(selectedPeriod: event.period));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_subscription?.cancel(); // Supaya tidak bocor memorinya
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
part of 'wind_speed_bloc.dart';
|
||||||
|
|
||||||
|
sealed class WindSpeedEvent extends Equatable {
|
||||||
|
const WindSpeedEvent();
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perintah untuk mulai dengerin data Firebase
|
||||||
|
class WatchWindSpeedStarted extends WindSpeedEvent {}
|
||||||
|
|
||||||
|
// Perintah kalau user ganti filter (Hari Ini, Minggu Ini, dll)
|
||||||
|
class WindSpeedPeriodChanged extends WindSpeedEvent {
|
||||||
|
final String period;
|
||||||
|
const WindSpeedPeriodChanged(this.period);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event internal: saat data baru masuk dari Firebase
|
||||||
|
class _WindSpeedUpdated extends WindSpeedEvent {
|
||||||
|
final MyWindSpeed data;
|
||||||
|
const _WindSpeedUpdated(this.data);
|
||||||
|
@override
|
||||||
|
List<Object> get props => [data];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
part of 'wind_speed_bloc.dart';
|
||||||
|
|
||||||
|
class WindSpeedState extends Equatable {
|
||||||
|
final double currentSpeed;
|
||||||
|
final String selectedPeriod;
|
||||||
|
final List<double> dailySpeeds; // Untuk grafik
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
const WindSpeedState({
|
||||||
|
this.currentSpeed = 0.0,
|
||||||
|
this.selectedPeriod = "Hari Ini",
|
||||||
|
this.dailySpeeds = const [],
|
||||||
|
this.isLoading = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
WindSpeedState copyWith({
|
||||||
|
double? currentSpeed,
|
||||||
|
String? selectedPeriod,
|
||||||
|
List<double>? dailySpeeds,
|
||||||
|
bool? isLoading,
|
||||||
|
}) {
|
||||||
|
return WindSpeedState(
|
||||||
|
currentSpeed: currentSpeed ?? this.currentSpeed,
|
||||||
|
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||||
|
dailySpeeds: dailySpeeds ?? this.dailySpeeds,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props =>
|
||||||
|
[currentSpeed, selectedPeriod, dailySpeeds, isLoading];
|
||||||
|
}
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
|
||||||
|
|
||||||
import '../../shared/widgets/monitoring_shared.dart';
|
|
||||||
|
|
||||||
class WindSpeedAnnualSection extends StatelessWidget {
|
|
||||||
final VoidCallback onExportAnnual;
|
|
||||||
|
|
||||||
const WindSpeedAnnualSection({
|
|
||||||
super.key,
|
|
||||||
required this.onExportAnnual,
|
|
||||||
});
|
|
||||||
final double _rataRata = 0.0;
|
|
||||||
final double _kecepatanMax = 0.0;
|
|
||||||
@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: [
|
|
||||||
Expanded(
|
|
||||||
child: StatCard(
|
|
||||||
title: "Rata-rata Tahunan",
|
|
||||||
value: this._rataRata.toStringAsFixed(2),
|
|
||||||
color: const Color.fromARGB(255, 76, 120, 175),
|
|
||||||
icon: Icons.air,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: StatCard(
|
|
||||||
title: "Kecepatan Max",
|
|
||||||
value: this._kecepatanMax.toStringAsFixed(2),
|
|
||||||
color: const Color.fromARGB(255, 109, 109, 109),
|
|
||||||
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: const Color.fromARGB(255, 224, 224, 224),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
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: const Color.fromARGB(255, 232, 240, 245),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.2),
|
|
||||||
blurRadius: 3,
|
|
||||||
spreadRadius: 0,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: const [
|
|
||||||
Text(
|
|
||||||
"Penjelasan Kecepatan Angin",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Color.fromARGB(255, 0, 0, 0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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: Color.fromARGB(255, 0, 0, 0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,253 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
|
||||||
|
|
||||||
import '../../shared/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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
side: BorderSide.none,
|
|
||||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
|
||||||
selectedColor:
|
|
||||||
const Color.fromARGB(255, 0, 136, 255),
|
|
||||||
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: const Color.fromARGB(255, 0, 145, 255),
|
|
||||||
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: const Color.fromARGB(255, 0, 191, 255),
|
|
||||||
strokeWidth: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
color: const Color.fromARGB(255, 0, 191, 255),
|
|
||||||
belowBarData: BarAreaData(
|
|
||||||
show: true,
|
|
||||||
color: const Color.fromARGB(255, 0, 115, 255)
|
|
||||||
.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
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);
|
|
||||||
|
|
||||||
/// ====== Current Speed bagian kode kecepatan angin ===== ///
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.withOpacity(0.12),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Kecepatan Angin",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"${currentSpeed.toStringAsFixed(2)} km/h",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Color.fromARGB(255, 0, 0, 0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
/// ==== bagian online offline status tampilan ==== ///
|
|
||||||
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: 10),
|
|
||||||
Text(
|
|
||||||
"Update: $formattedTime",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
|
||||||
|
|
||||||
import '../../shared/widgets/monitoring_shared.dart';
|
|
||||||
|
|
||||||
class WindSpeedAnnualSection extends StatelessWidget {
|
|
||||||
final VoidCallback onExportAnnual;
|
|
||||||
|
|
||||||
const WindSpeedAnnualSection({
|
|
||||||
super.key,
|
|
||||||
required this.onExportAnnual,
|
|
||||||
});
|
|
||||||
final double _rataRata = 0.0;
|
|
||||||
final double _kecepatanMax = 0.0;
|
|
||||||
@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: [
|
|
||||||
Expanded(
|
|
||||||
child: StatCard(
|
|
||||||
title: "Rata-rata Tahunan",
|
|
||||||
value: this._rataRata.toStringAsFixed(2),
|
|
||||||
color: const Color.fromARGB(255, 76, 120, 175),
|
|
||||||
icon: Icons.air,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: StatCard(
|
|
||||||
title: "Kecepatan Max",
|
|
||||||
value: this._kecepatanMax.toStringAsFixed(2),
|
|
||||||
color: const Color.fromARGB(255, 109, 109, 109),
|
|
||||||
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: const Color.fromARGB(255, 224, 224, 224),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,253 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
|
||||||
|
|
||||||
import '../../shared/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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
side: BorderSide.none,
|
|
||||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
|
||||||
selectedColor:
|
|
||||||
const Color.fromARGB(255, 0, 136, 255),
|
|
||||||
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: const Color.fromARGB(255, 0, 145, 255),
|
|
||||||
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: const Color.fromARGB(255, 0, 191, 255),
|
|
||||||
strokeWidth: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
color: const Color.fromARGB(255, 0, 191, 255),
|
|
||||||
belowBarData: BarAreaData(
|
|
||||||
show: true,
|
|
||||||
color: const Color.fromARGB(255, 0, 115, 255)
|
|
||||||
.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,272 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import '../';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../shared/widgets/monitoring_shared.dart';
|
|
||||||
import 'views/annual_section.dart';
|
|
||||||
import 'views/explanation.dart';
|
|
||||||
import 'views/graph_block.dart';
|
|
||||||
import 'views/status_block.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
|
|
||||||
class WindSpeedMonitoringScreen extends StatefulWidget {
|
|
||||||
const WindSpeedMonitoringScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<WindSpeedMonitoringScreen> createState() =>
|
|
||||||
_WindSpeedMonitoringScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
|
|
||||||
String _selectedPeriod = "Hari Ini";
|
|
||||||
|
|
||||||
// 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).toLocal();
|
|
||||||
|
|
||||||
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 = [];
|
|
||||||
|
|
||||||
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 = [];
|
|
||||||
final maxSpeed = [];
|
|
||||||
|
|
||||||
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: Text(
|
|
||||||
"Wind Speed",
|
|
||||||
style: GoogleFonts.rubik(
|
|
||||||
color: Colors.black, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
backgroundColor: const Color.fromARGB(37, 158, 158, 158),
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (_lastUpdateTime != null)
|
|
||||||
WindSpeedStatusBlock(
|
|
||||||
currentSpeed: _currentSpeed,
|
|
||||||
isOnline: _isOnline,
|
|
||||||
lastUpdateTime: _lastUpdateTime!,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
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,262 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:firebase_database/firebase_database.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../shared/widgets/monitoring_shared.dart';
|
|
||||||
import 'views/annual_section.dart';
|
|
||||||
import 'views/explanation.dart';
|
|
||||||
import 'views/graph_block.dart';
|
|
||||||
import 'views/status_block.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
|
|
||||||
class WindSpeedMonitoringScreen extends StatefulWidget {
|
|
||||||
const WindSpeedMonitoringScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<WindSpeedMonitoringScreen> createState() =>
|
|
||||||
_WindSpeedMonitoringScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
|
|
||||||
String _selectedPeriod = "Hari Ini";
|
|
||||||
|
|
||||||
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).toLocal();
|
|
||||||
|
|
||||||
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 = [];
|
|
||||||
|
|
||||||
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 = [];
|
|
||||||
final maxSpeed = [];
|
|
||||||
|
|
||||||
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: Text(
|
|
||||||
"Wind Speed",
|
|
||||||
style: GoogleFonts.rubik(
|
|
||||||
color: Colors.black, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
backgroundColor: const Color.fromARGB(37, 158, 158, 158),
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (_lastUpdateTime != null)
|
|
||||||
WindSpeedStatusBlock(
|
|
||||||
currentSpeed: _currentSpeed,
|
|
||||||
isOnline: _isOnline,
|
|
||||||
lastUpdateTime: _lastUpdateTime!,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,3 +2,4 @@ library monitoring_repository;
|
||||||
|
|
||||||
export 'src/monitoring_repo.dart';
|
export 'src/monitoring_repo.dart';
|
||||||
export 'src/firebase_monitoring_repo.dart';
|
export 'src/firebase_monitoring_repo.dart';
|
||||||
|
export 'src/models/models.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:firebase_database/firebase_database.dart';
|
|
||||||
import 'models/models.dart';
|
import 'models/models.dart';
|
||||||
|
|
||||||
abstract class MonitoringRepository {
|
abstract class MonitoringRepository {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue