pembaruan wind_speed_screen.dart

tampilan halaman kecepatan angin memperbarui tampilan dan period grafik dari harian, mingguan dan bulanan
This commit is contained in:
kleponijo 2026-04-15 21:03:16 +07:00
parent 410c51722b
commit 2d1ccdb8b2
6 changed files with 182 additions and 13 deletions

View File

@ -17,21 +17,27 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
(event, emit) async {
emit(state.copyWith(isLoading: true));
// 1. Ambil semua history
final history = await _repository.getSensorHistory(
'anemometer/history', (json) => MyWindSpeed.fromJson(json));
// 2. Petakan ke 24 jam (Harian)
final List<double> dailyGraph = _mapHistoryToDaily(history);
emit(state.copyWith(
dailySpeeds: dailyGraph,
isLoading: false,
));
// 2. Monitoring Real-time
await emit.forEach<MyWindSpeed>(
_repository.getSensorStream(
'anemometer/realtime', (json) => MyWindSpeed.fromJson(json)),
onData: (data) {
// Mengambil list lama dan menambah data baru untuk grafik
final updatedSpeeds = List<double>.from(state.dailySpeeds)
..add(data.speed);
// Batasi jumlah data di grafik (misal cuma simpan 20 data terakhir agar tidak berat)
if (updatedSpeeds.length > 20) {
updatedSpeeds.removeAt(0);
}
return state.copyWith(
currentSpeed: data.speed,
dailySpeeds: updatedSpeeds, // Update list grafik
isLoading: false,
);
},
@ -41,13 +47,78 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
transformer: restartable(),
);
on<WindSpeedPeriodChanged>((event, emit) {
on<WindSpeedPeriodChanged>((event, emit) async {
emit(state.copyWith(selectedPeriod: event.period));
// Ambil ulang history dari database
final history = await _repository.getSensorHistory(
'anemometer/history', (json) => MyWindSpeed.fromJson(json));
List<double> updatedGraph;
// Pilih mapper berdasarkan pilihan user
if (event.period == "Minggu Ini") {
updatedGraph = _mapHistoryToWeekly(history);
} else if (event.period == "Bulan Ini") {
updatedGraph = _mapHistoryToMonthly(history);
} else {
updatedGraph = _mapHistoryToDaily(history);
}
emit(state.copyWith(
dailySpeeds: updatedGraph,
isLoading: false,
));
});
}
@override
Future<void> close() {
return super.close();
}
Future<void> close() => super.close();
}
List<double> _mapHistoryToDaily(List<MyWindSpeed> history) {
// Buat list 24 angka nol (representasi jam 00:00 - 23:00)
List<double> slots = List.generate(24, (_) => 0.0);
for (var item in history) {
// Ubah timestamp UTC ke jam lokal
DateTime date = item.timestamp;
// Jika data ini adalah data hari ini, masukkan ke slot jamnya
if (date.day == DateTime.now().day) {
slots[date.hour] = item.speed;
}
}
return slots;
}
// Mapper Mingguan (7 Slot: Senin - Minggu)
List<double> _mapHistoryToWeekly(List<MyWindSpeed> history) {
List<double> slots = List.generate(7, (_) => 0.0);
final now = DateTime.now();
// Mencari awal minggu ini (Senin)
DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1));
startOfWeek = DateTime(startOfWeek.year, startOfWeek.month, startOfWeek.day);
for (var item in history) {
if (item.timestamp.isAfter(startOfWeek)) {
// index 0 = Senin, dst.
slots[item.timestamp.weekday - 1] = item.speed;
}
}
return slots;
}
// Mapper Bulanan (Sesuai jumlah hari di bulan ini)
List<double> _mapHistoryToMonthly(List<MyWindSpeed> history) {
final now = DateTime.now();
final daysInMonth = DateTime(now.year, now.month + 1, 0).day;
List<double> slots = List.generate(daysInMonth, (_) => 0.0);
for (var item in history) {
if (item.timestamp.month == now.month && item.timestamp.year == now.year) {
// index 0 = Tanggal 1, dst.
slots[item.timestamp.day - 1] = item.speed;
}
}
return slots;
}

View File

@ -1 +1,57 @@
/// === Tombol Filter Jam/Hari/Minggu === ///
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../blocs/wind_speed_bloc.dart';
class PeriodSelector extends StatelessWidget {
const PeriodSelector({super.key});
@override
Widget build(BuildContext context) {
// Kita ambil state saat ini untuk tahu periode mana yang aktif
final selectedPeriod =
context.select((WindSpeedBloc bloc) => bloc.state.selectedPeriod);
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildTab(context, "Hari Ini", selectedPeriod),
_buildTab(context, "Minggu Ini", selectedPeriod),
_buildTab(context, "Bulan Ini", selectedPeriod),
],
),
);
}
Widget _buildTab(BuildContext context, String label, String current) {
bool isActive = label == current;
return GestureDetector(
onTap: () {
// Kirim event ke Bloc saat tombol diklik
context.read<WindSpeedBloc>().add(WindSpeedPeriodChanged(label));
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isActive ? Colors.blue : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Text(
label,
style: TextStyle(
color: isActive ? Colors.white : Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
);
}
}

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
// import 'package:fl_chart/fl_chart.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/wind_speed_bloc.dart';
import './widgets/wind_speed_chart_widget.dart';
import 'widgets/period_selector.dart';
class WindSpeedScreen extends StatelessWidget {
const WindSpeedScreen({super.key});
@ -37,10 +38,13 @@ class WindSpeedScreen extends StatelessWidget {
_buildMainSpeedDisplay(state.currentSpeed),
const SizedBox(height: 30),
const Text("Tren Real-time",
const Text("Tren Kecepatan",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 15),
const PeriodSelector(),
const SizedBox(height: 15),
WindSpeedChartWidget(dailySpeeds: state.dailySpeeds),
const SizedBox(height: 30),

View File

@ -38,4 +38,28 @@ class FirebaseMonitoringRepo implements MonitoringRepository {
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
return mapper(data);
}
@override
Future<List<T>> getSensorHistory<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
) async {
try {
// Ambil data dari path history
final snapshot = await _db.ref(path).get();
if (snapshot.exists && snapshot.value is Map) {
final Map<dynamic, dynamic> data = snapshot.value as Map;
// Karena RTDB push() menghasilkan Map dengan ID unik,
// kita ambil values-nya saja dan ubah jadi List objek
return data.values.map((item) {
return mapper(item as Map<dynamic, dynamic>);
}).toList();
}
return [];
} catch (e) {
return [];
}
}
}

View File

@ -9,6 +9,12 @@ class MyWindSpeed {
required this.timestamp,
});
static final empty = MyWindSpeed(
speed: 0.0,
pulse: 0,
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
);
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
return MyWindSpeed(
speed: (json['speed'] ?? 0).toDouble(),

View File

@ -1,13 +1,21 @@
import 'models/models.dart';
abstract class MonitoringRepository {
// fungsi untuk ambil data berkali/streaming
Stream<T> getSensorStream<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
);
// fungsi untuk ambil sensor sekali
Future<T> getSensorSnapshot<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
);
// Fungsi untuk ambil riwayat (List)
Future<List<T>> getSensorHistory<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
);
}