oke
This commit is contained in:
parent
828645da5b
commit
95b1759344
|
|
@ -0,0 +1,82 @@
|
||||||
|
class TimeSeriesMapper {
|
||||||
|
/// =========================
|
||||||
|
/// 📅 DAILY (24 JAM)
|
||||||
|
/// =========================
|
||||||
|
static List<double> toDaily<T>({
|
||||||
|
required List<T> data,
|
||||||
|
required DateTime Function(T) getTime,
|
||||||
|
required double Function(T) getValue,
|
||||||
|
}) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final slots = List<double>.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<double> toWeekly<T>({
|
||||||
|
required List<T> data,
|
||||||
|
required DateTime Function(T) getTime,
|
||||||
|
required double Function(T) getValue,
|
||||||
|
}) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final slots = List<double>.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<double> toMonthly<T>({
|
||||||
|
required List<T> 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<double>.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<EvaporasiEvent, EvaporasiState> {
|
||||||
|
final MonitoringRepository _repository;
|
||||||
|
StreamSubscription<Evaporasi>? _subscription;
|
||||||
|
|
||||||
|
EvaporasiBloc({required MonitoringRepository repository})
|
||||||
|
: _repository = repository,
|
||||||
|
super(const EvaporasiState()) {
|
||||||
|
|
||||||
|
on<WatchEvaporasiStarted>(_onStarted);
|
||||||
|
on<_EvaporasiRealtimeUpdated>(_onRealtimeUpdated);
|
||||||
|
on<EvaporasiPeriodChanged>(_onPeriodChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 🚀 START
|
||||||
|
/// =========================
|
||||||
|
Future<void> _onStarted(
|
||||||
|
WatchEvaporasiStarted event,
|
||||||
|
Emitter<EvaporasiState> 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<EvaporasiState> emit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
final updated = List<double>.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<void> _onPeriodChanged(
|
||||||
|
EvaporasiPeriodChanged event,
|
||||||
|
Emitter<EvaporasiState> emit,
|
||||||
|
) async {
|
||||||
|
|
||||||
|
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
|
||||||
|
|
||||||
|
final history = state.history;
|
||||||
|
|
||||||
|
List<double> 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<void> close() async {
|
||||||
|
await _subscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// INTERNAL EVENT
|
||||||
|
class _EvaporasiRealtimeUpdated extends EvaporasiEvent {
|
||||||
|
final Evaporasi data;
|
||||||
|
|
||||||
|
const _EvaporasiRealtimeUpdated(this.data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [data];
|
||||||
|
}
|
||||||
|
|
@ -3,138 +3,149 @@ import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||||
|
import '../../../../core/utils/time_series_mapper.dart';
|
||||||
|
|
||||||
part 'wind_speed_event.dart';
|
part 'wind_speed_event.dart';
|
||||||
part 'wind_speed_state.dart';
|
part 'wind_speed_state.dart';
|
||||||
|
|
||||||
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
final MonitoringRepository _repository;
|
final MonitoringRepository _repository;
|
||||||
|
StreamSubscription<MyWindSpeed>? _subscription;
|
||||||
|
|
||||||
WindSpeedBloc({required MonitoringRepository repository})
|
WindSpeedBloc({required MonitoringRepository repository})
|
||||||
: _repository = repository,
|
: _repository = repository,
|
||||||
super(const WindSpeedState()) {
|
super(const WindSpeedState()) {
|
||||||
// Handler saat aplikasi minta mulai monitoring
|
|
||||||
on<WatchWindSpeedStarted>(
|
|
||||||
(event, emit) async {
|
|
||||||
emit(state.copyWith(isLoading: true));
|
|
||||||
|
|
||||||
// 1. Ambil semua history
|
/// 🔥 START MONITORING
|
||||||
final history = await _repository.getSensorHistory(
|
on<WatchWindSpeedStarted>(_onStarted, transformer: restartable());
|
||||||
'anemometer/history', (json) => MyWindSpeed.fromJson(json));
|
|
||||||
|
|
||||||
// 2. Petakan ke 24 jam (Harian)
|
/// 🔥 REALTIME UPDATE
|
||||||
final List<double> dailyGraph = _mapHistoryToDaily(history);
|
on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated);
|
||||||
|
|
||||||
emit(state.copyWith(
|
/// 🔥 CHANGE PERIOD
|
||||||
dailySpeeds: dailyGraph,
|
on<WindSpeedPeriodChanged>(_onPeriodChanged);
|
||||||
isLoading: false,
|
}
|
||||||
));
|
|
||||||
|
|
||||||
// 2. Monitoring Real-time
|
/// =========================
|
||||||
await emit.forEach<MyWindSpeed>(
|
/// 🚀 START
|
||||||
_repository.getSensorStream(
|
/// =========================
|
||||||
'anemometer/realtime', (json) => MyWindSpeed.fromJson(json)),
|
Future<void> _onStarted(
|
||||||
onData: (data) {
|
WatchWindSpeedStarted event,
|
||||||
// 1. Ambil list kecepatan yang ada saat ini di state
|
Emitter<WindSpeedState> emit,
|
||||||
final updatedSpeeds = List<double>.from(state.dailySpeeds);
|
) async {
|
||||||
|
|
||||||
// 2. Tentukan di index mana data ini harus masuk (misal berdasarkan menit saat ini)
|
emit(state.copyWith(isLoading: true));
|
||||||
final int index = DateTime.now().minute;
|
|
||||||
|
|
||||||
// 3. Update nilai di index tersebut
|
/// 1. Ambil history SEKALI
|
||||||
if (index < updatedSpeeds.length) {
|
final history = await _repository.getSensorHistory(
|
||||||
updatedSpeeds[index] = data.speed;
|
'anemometer/history',
|
||||||
}
|
(json) => MyWindSpeed.fromJson(json),
|
||||||
|
|
||||||
return state.copyWith(
|
|
||||||
currentSpeed: data.speed,
|
|
||||||
dailySpeeds: updatedSpeeds,
|
|
||||||
isLoading: false,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (error, stackTrace) => state.copyWith(isLoading: false),
|
|
||||||
); // emit forEach
|
|
||||||
},
|
|
||||||
transformer: restartable(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
on<WindSpeedPeriodChanged>((event, emit) async {
|
/// 2. Mapping default (harian)
|
||||||
emit(state.copyWith(selectedPeriod: event.period));
|
final dailyGraph = TimeSeriesMapper.toDaily(
|
||||||
// Ambil ulang history dari database
|
data: history,
|
||||||
final history = await _repository.getSensorHistory(
|
getTime: (e) => e.timestamp,
|
||||||
'anemometer/history', (json) => MyWindSpeed.fromJson(json));
|
getValue: (e) => e.speed,
|
||||||
List<double> updatedGraph;
|
);
|
||||||
|
|
||||||
// Pilih mapper berdasarkan pilihan user
|
emit(state.copyWith(
|
||||||
if (event.period == "Minggu Ini") {
|
history: history,
|
||||||
updatedGraph = _mapHistoryToWeekly(history);
|
dailySpeeds: dailyGraph,
|
||||||
} else if (event.period == "Bulan Ini") {
|
isLoading: false,
|
||||||
updatedGraph = _mapHistoryToMonthly(history);
|
));
|
||||||
} else {
|
|
||||||
updatedGraph = _mapHistoryToDaily(history);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(state.copyWith(
|
/// 3. Start realtime stream (manual subscription)
|
||||||
dailySpeeds: updatedGraph,
|
await _subscription?.cancel();
|
||||||
isLoading: false,
|
|
||||||
));
|
_subscription = _repository
|
||||||
|
.getSensorStream(
|
||||||
|
'anemometer/realtime',
|
||||||
|
(json) => MyWindSpeed.fromJson(json),
|
||||||
|
)
|
||||||
|
.listen((data) {
|
||||||
|
add(_WindSpeedRealtimeUpdated(data));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// ⚡ REALTIME UPDATE
|
||||||
|
/// =========================
|
||||||
|
void _onRealtimeUpdated(
|
||||||
|
_WindSpeedRealtimeUpdated event,
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
final updatedSpeeds = List<double>.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<void> _onPeriodChanged(
|
||||||
|
WindSpeedPeriodChanged event,
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
) async {
|
||||||
|
|
||||||
|
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
|
||||||
|
|
||||||
|
final history = state.history;
|
||||||
|
|
||||||
|
List<double> 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
|
@override
|
||||||
Future<void> close() => super.close();
|
Future<void> close() async {
|
||||||
}
|
await _subscription?.cancel();
|
||||||
|
return 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); // mode asli
|
|
||||||
// Kita buat 60 slot (representasi menit 0-59) agar tiap menit kelihatan titik baru
|
|
||||||
// List<double> 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
|
|
||||||
}
|
}
|
||||||
return slots;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapper Mingguan (7 Slot: Senin - Minggu)
|
/// =========================
|
||||||
List<double> _mapHistoryToWeekly(List<MyWindSpeed> history) {
|
/// 🔒 INTERNAL EVENT (PRIVATE)
|
||||||
List<double> slots = List.generate(7, (_) => 0.0);
|
/// =========================
|
||||||
final now = DateTime.now();
|
class _WindSpeedRealtimeUpdated extends WindSpeedEvent {
|
||||||
|
final MyWindSpeed data;
|
||||||
|
|
||||||
// Mencari awal minggu ini (Senin)
|
const _WindSpeedRealtimeUpdated(this.data);
|
||||||
DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1));
|
|
||||||
startOfWeek = DateTime(startOfWeek.year, startOfWeek.month, startOfWeek.day);
|
|
||||||
|
|
||||||
for (var item in history) {
|
@override
|
||||||
if (item.timestamp.isAfter(startOfWeek)) {
|
List<Object> get props => [data];
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
@ -5,12 +5,14 @@ class WindSpeedState extends Equatable {
|
||||||
final String selectedPeriod;
|
final String selectedPeriod;
|
||||||
final List<double> dailySpeeds; // Untuk grafik
|
final List<double> dailySpeeds; // Untuk grafik
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
final List<MyWindSpeed> history;
|
||||||
|
|
||||||
const WindSpeedState({
|
const WindSpeedState({
|
||||||
this.currentSpeed = 0.0,
|
this.currentSpeed = 0.0,
|
||||||
this.selectedPeriod = "Hari Ini",
|
this.selectedPeriod = "Hari Ini",
|
||||||
this.dailySpeeds = const [],
|
this.dailySpeeds = const [],
|
||||||
this.isLoading = true,
|
this.isLoading = true,
|
||||||
|
this.history = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
WindSpeedState copyWith({
|
WindSpeedState copyWith({
|
||||||
|
|
@ -18,16 +20,18 @@ class WindSpeedState extends Equatable {
|
||||||
String? selectedPeriod,
|
String? selectedPeriod,
|
||||||
List<double>? dailySpeeds,
|
List<double>? dailySpeeds,
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
|
List<MyWindSpeed>? history,
|
||||||
}) {
|
}) {
|
||||||
return WindSpeedState(
|
return WindSpeedState(
|
||||||
currentSpeed: currentSpeed ?? this.currentSpeed,
|
currentSpeed: currentSpeed ?? this.currentSpeed,
|
||||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||||
dailySpeeds: dailySpeeds ?? this.dailySpeeds,
|
dailySpeeds: dailySpeeds ?? this.dailySpeeds,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
history: history ?? this.history,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props =>
|
List<Object> get props =>
|
||||||
[currentSpeed, selectedPeriod, dailySpeeds, isLoading];
|
[currentSpeed, selectedPeriod, dailySpeeds, isLoading, history];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue