From 95b1759344393c760a6a6d2119870e0833788d81 Mon Sep 17 00:00:00 2001 From: e32232340-Nadif <158453666+kleponijo@users.noreply.github.com> Date: Sun, 19 Apr 2026 07:39:04 +0700 Subject: [PATCH] oke --- lib/core/utils/time_series_mapper.dart | 82 +++++++ .../evaporasi/blocs/evaporasi_bloc.dart | 140 +++++++++++ .../{evaporasi.dart => evaporasi_screen.dart} | 0 .../wind_speed/blocs/wind_speed_bloc.dart | 225 +++++++++--------- .../wind_speed/blocs/wind_speed_state.dart | 6 +- 5 files changed, 345 insertions(+), 108 deletions(-) create mode 100644 lib/core/utils/time_series_mapper.dart rename lib/screens/monitoring/evaporasi/views/{evaporasi.dart => evaporasi_screen.dart} (100%) diff --git a/lib/core/utils/time_series_mapper.dart b/lib/core/utils/time_series_mapper.dart new file mode 100644 index 0000000..fd2df54 --- /dev/null +++ b/lib/core/utils/time_series_mapper.dart @@ -0,0 +1,82 @@ +class TimeSeriesMapper { + /// ========================= + /// 📅 DAILY (24 JAM) + /// ========================= + static List toDaily({ + required List data, + required DateTime Function(T) getTime, + required double Function(T) getValue, + }) { + final now = DateTime.now(); + final slots = List.filled(24, 0.0); + + for (final item in data) { + final time = getTime(item); + + if (_isSameDay(time, now)) { + slots[time.hour] = getValue(item); + } + } + + return slots; + } + + /// ========================= + /// 📅 WEEKLY (7 HARI) + /// ========================= + static List toWeekly({ + required List data, + required DateTime Function(T) getTime, + required double Function(T) getValue, + }) { + final now = DateTime.now(); + final slots = List.filled(7, 0.0); + + DateTime startOfWeek = + now.subtract(Duration(days: now.weekday - 1)); + startOfWeek = DateTime( + startOfWeek.year, startOfWeek.month, startOfWeek.day); + + for (final item in data) { + final time = getTime(item); + + if (time.isAfter(startOfWeek)) { + slots[time.weekday - 1] = getValue(item); + } + } + + return slots; + } + + /// ========================= + /// 📅 MONTHLY + /// ========================= + static List toMonthly({ + required List data, + required DateTime Function(T) getTime, + required double Function(T) getValue, + }) { + final now = DateTime.now(); + final daysInMonth = DateTime(now.year, now.month + 1, 0).day; + final slots = List.filled(daysInMonth, 0.0); + + for (final item in data) { + final time = getTime(item); + + if (time.month == now.month && time.year == now.year) { + slots[time.day - 1] = getValue(item); + } + } + + return slots; + } + + /// ========================= + /// 🧠 HELPER + /// ========================= + static bool _isSameDay(DateTime a, DateTime b) { + return a.year == b.year && + a.month == b.month && + a.day == b.day; + } +} \ No newline at end of file diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart index e69de29..fd8fafb 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:monitoring_repository/monitoring_repository.dart'; + +import '../../../core/utils/time_series_mapper.dart'; + +part 'evaporasi_event.dart'; +part 'evaporasi_state.dart'; + +class EvaporasiBloc extends Bloc { + final MonitoringRepository _repository; + StreamSubscription? _subscription; + + EvaporasiBloc({required MonitoringRepository repository}) + : _repository = repository, + super(const EvaporasiState()) { + + on(_onStarted); + on<_EvaporasiRealtimeUpdated>(_onRealtimeUpdated); + on(_onPeriodChanged); + } + + /// ========================= + /// 🚀 START + /// ========================= + Future _onStarted( + WatchEvaporasiStarted event, + Emitter emit, + ) async { + + emit(state.copyWith(isLoading: true)); + + final history = await _repository.getSensorHistory( + 'evaporasi/history', + (json) => Evaporasi.fromJson(json), + ); + + final dailyGraph = TimeSeriesMapper.toDaily( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.value, // ⚠️ sesuaikan nama field + ); + + emit(state.copyWith( + history: history, + dailyValues: dailyGraph, + isLoading: false, + )); + + await _subscription?.cancel(); + + _subscription = _repository + .getSensorStream( + 'evaporasi/realtime', + (json) => Evaporasi.fromJson(json), + ) + .listen((data) { + add(_EvaporasiRealtimeUpdated(data)); + }); + } + + /// ========================= + /// ⚡ REALTIME + /// ========================= + void _onRealtimeUpdated( + _EvaporasiRealtimeUpdated event, + Emitter emit, + ) { + + final updated = List.from(state.dailyValues); + + final index = DateTime.now().hour; + + if (index < updated.length) { + updated[index] = event.data.value; // ⚠️ sesuaikan field + } + + emit(state.copyWith( + currentValue: event.data.value, + dailyValues: updated, + )); + } + + /// ========================= + /// 📊 PERIOD + /// ========================= + Future _onPeriodChanged( + EvaporasiPeriodChanged event, + Emitter emit, + ) async { + + emit(state.copyWith(isLoading: true, selectedPeriod: event.period)); + + final history = state.history; + + List updated; + + if (event.period == "Minggu Ini") { + updated = TimeSeriesMapper.toWeekly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.value, + ); + } else if (event.period == "Bulan Ini") { + updated = TimeSeriesMapper.toMonthly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.value, + ); + } else { + updated = TimeSeriesMapper.toDaily( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.value, + ); + } + + emit(state.copyWith( + dailyValues: updated, + isLoading: false, + )); + } + + @override + Future close() async { + await _subscription?.cancel(); + return super.close(); + } +} + +/// INTERNAL EVENT +class _EvaporasiRealtimeUpdated extends EvaporasiEvent { + final Evaporasi data; + + const _EvaporasiRealtimeUpdated(this.data); + + @override + List get props => [data]; +} \ No newline at end of file diff --git a/lib/screens/monitoring/evaporasi/views/evaporasi.dart b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart similarity index 100% rename from lib/screens/monitoring/evaporasi/views/evaporasi.dart rename to lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart index 2ae3c1f..f7b18ef 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart @@ -3,138 +3,149 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:monitoring_repository/monitoring_repository.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; +import '../../../../core/utils/time_series_mapper.dart'; + part 'wind_speed_event.dart'; part 'wind_speed_state.dart'; class WindSpeedBloc extends Bloc { final MonitoringRepository _repository; + StreamSubscription? _subscription; WindSpeedBloc({required MonitoringRepository repository}) : _repository = repository, super(const WindSpeedState()) { - // Handler saat aplikasi minta mulai monitoring - on( - (event, emit) async { - emit(state.copyWith(isLoading: true)); - // 1. Ambil semua history - final history = await _repository.getSensorHistory( - 'anemometer/history', (json) => MyWindSpeed.fromJson(json)); + /// 🔥 START MONITORING + on(_onStarted, transformer: restartable()); - // 2. Petakan ke 24 jam (Harian) - final List dailyGraph = _mapHistoryToDaily(history); + /// 🔥 REALTIME UPDATE + on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated); - emit(state.copyWith( - dailySpeeds: dailyGraph, - isLoading: false, - )); + /// 🔥 CHANGE PERIOD + on(_onPeriodChanged); + } - // 2. Monitoring Real-time - await emit.forEach( - _repository.getSensorStream( - 'anemometer/realtime', (json) => MyWindSpeed.fromJson(json)), - onData: (data) { - // 1. Ambil list kecepatan yang ada saat ini di state - final updatedSpeeds = List.from(state.dailySpeeds); + /// ========================= + /// 🚀 START + /// ========================= + Future _onStarted( + WatchWindSpeedStarted event, + Emitter emit, + ) async { - // 2. Tentukan di index mana data ini harus masuk (misal berdasarkan menit saat ini) - final int index = DateTime.now().minute; + emit(state.copyWith(isLoading: true)); - // 3. Update nilai di index tersebut - if (index < updatedSpeeds.length) { - updatedSpeeds[index] = data.speed; - } - - return state.copyWith( - currentSpeed: data.speed, - dailySpeeds: updatedSpeeds, - isLoading: false, - ); - }, - onError: (error, stackTrace) => state.copyWith(isLoading: false), - ); // emit forEach - }, - transformer: restartable(), + /// 1. Ambil history SEKALI + final history = await _repository.getSensorHistory( + 'anemometer/history', + (json) => MyWindSpeed.fromJson(json), ); - on((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 updatedGraph; + /// 2. Mapping default (harian) + final dailyGraph = TimeSeriesMapper.toDaily( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.speed, + ); - // 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( + history: history, + dailySpeeds: dailyGraph, + isLoading: false, + )); - emit(state.copyWith( - dailySpeeds: updatedGraph, - isLoading: false, - )); + /// 3. Start realtime stream (manual subscription) + await _subscription?.cancel(); + + _subscription = _repository + .getSensorStream( + 'anemometer/realtime', + (json) => MyWindSpeed.fromJson(json), + ) + .listen((data) { + add(_WindSpeedRealtimeUpdated(data)); }); } + /// ========================= + /// ⚡ REALTIME UPDATE + /// ========================= + void _onRealtimeUpdated( + _WindSpeedRealtimeUpdated event, + Emitter emit, + ) { + + final updatedSpeeds = List.from(state.dailySpeeds); + + /// 🔥 FIX: pakai JAM bukan menit + final int index = DateTime.now().hour; + + if (index < updatedSpeeds.length) { + updatedSpeeds[index] = event.data.speed; + } + + emit(state.copyWith( + currentSpeed: event.data.speed, + dailySpeeds: updatedSpeeds, + )); + } + + /// ========================= + /// 📊 CHANGE PERIOD + /// ========================= + Future _onPeriodChanged( + WindSpeedPeriodChanged event, + Emitter emit, + ) async { + + emit(state.copyWith(isLoading: true, selectedPeriod: event.period)); + + final history = state.history; + + List updatedGraph; + + if (event.period == "Minggu Ini") { + updatedGraph = TimeSeriesMapper.toWeekly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.speed, + ); +} else if (event.period == "Bulan Ini") { + updatedGraph = TimeSeriesMapper.toMonthly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.speed, + ); +} else { + updatedGraph = TimeSeriesMapper.toDaily( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.speed, + ); +} + + emit(state.copyWith( + dailySpeeds: updatedGraph, + isLoading: false, + )); + } + @override - Future close() => super.close(); -} - -List _mapHistoryToDaily(List history) { - // Buat list 24 angka nol (representasi jam 00:00 - 23:00) - List slots = List.generate(24, (_) => 0.0); // mode asli - // Kita buat 60 slot (representasi menit 0-59) agar tiap menit kelihatan titik baru - // List slots = List.generate(60, (_) => 0.0); // mode percobaan - - for (var item in history) { - // Ubah timestamp UTC ke jam lokal - DateTime date = item.timestamp; // sudah dateTime dari model - - // Jika data ini adalah data hari ini, masukkan ke slot jamnya - if (date.day == DateTime.now().day) { - slots[date.hour] = item.speed; - } // mode asli - // pakai menit untuk percobaan (date.minute) sebagai index supaya kelihatan pergerakannya - // if (date.day == DateTime.now().day) { - // slots[date.minute] = item.speed; - // } // mode percobaan/testing + Future close() async { + await _subscription?.cancel(); + return super.close(); } - return slots; } -// Mapper Mingguan (7 Slot: Senin - Minggu) -List _mapHistoryToWeekly(List history) { - List slots = List.generate(7, (_) => 0.0); - final now = DateTime.now(); +/// ========================= +/// 🔒 INTERNAL EVENT (PRIVATE) +/// ========================= +class _WindSpeedRealtimeUpdated extends WindSpeedEvent { + final MyWindSpeed data; - // Mencari awal minggu ini (Senin) - DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1)); - startOfWeek = DateTime(startOfWeek.year, startOfWeek.month, startOfWeek.day); + const _WindSpeedRealtimeUpdated(this.data); - 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 _mapHistoryToMonthly(List history) { - final now = DateTime.now(); - final daysInMonth = DateTime(now.year, now.month + 1, 0).day; - List 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; -} + @override + List get props => [data]; +} \ No newline at end of file diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart index fa9b167..becd2e4 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart @@ -5,12 +5,14 @@ class WindSpeedState extends Equatable { final String selectedPeriod; final List dailySpeeds; // Untuk grafik final bool isLoading; + final List history; const WindSpeedState({ this.currentSpeed = 0.0, this.selectedPeriod = "Hari Ini", this.dailySpeeds = const [], this.isLoading = true, + this.history = const [], }); WindSpeedState copyWith({ @@ -18,16 +20,18 @@ class WindSpeedState extends Equatable { String? selectedPeriod, List? dailySpeeds, bool? isLoading, + List? history, }) { return WindSpeedState( currentSpeed: currentSpeed ?? this.currentSpeed, selectedPeriod: selectedPeriod ?? this.selectedPeriod, dailySpeeds: dailySpeeds ?? this.dailySpeeds, isLoading: isLoading ?? this.isLoading, + history: history ?? this.history, ); } @override List get props => - [currentSpeed, selectedPeriod, dailySpeeds, isLoading]; + [currentSpeed, selectedPeriod, dailySpeeds, isLoading, history]; }