mirror of
https://github.com/PranshulGG/WeatherMaster.git
synced 2026-10-21 18:43:03 -05:00
319 lines
9.5 KiB
Dart
319 lines
9.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import '../notifiers/unit_settings_notifier.dart';
|
|
import '../utils/unit_converter.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class RainBlock extends StatelessWidget {
|
|
final List<String> hourlyTime;
|
|
final List<double> hourlyPrecp;
|
|
final List<dynamic> hourlyPrecpProb;
|
|
final int selectedContainerBgIndex;
|
|
final String timezone;
|
|
final String utcOffsetSeconds;
|
|
|
|
const RainBlock({
|
|
super.key,
|
|
required this.hourlyTime,
|
|
required this.hourlyPrecp,
|
|
required this.selectedContainerBgIndex,
|
|
required this.timezone,
|
|
required this.utcOffsetSeconds,
|
|
required this.hourlyPrecpProb
|
|
});
|
|
|
|
|
|
|
|
int get _currentIndex {
|
|
int offsetSeconds = int.parse(utcOffsetSeconds);
|
|
DateTime utcNow = DateTime.now().toUtc();
|
|
DateTime now = utcNow.add(Duration(seconds: offsetSeconds));
|
|
|
|
|
|
now = DateTime(
|
|
now.year,
|
|
now.month,
|
|
now.day,
|
|
now.hour,
|
|
now.minute,
|
|
now.second,
|
|
now.millisecond,
|
|
now.microsecond,
|
|
);
|
|
|
|
|
|
for (int i = 0; i < hourlyTime.length; i++) {
|
|
final dt = DateTime.parse(hourlyTime[i]);
|
|
if (!dt.isBefore(now)) return i;
|
|
}
|
|
return 0; // fallback if all are in the past
|
|
}
|
|
|
|
List<String> get next12Time => hourlyTime.skip(_currentIndex).take(12).toList();
|
|
List<double> get next12Precp => hourlyPrecp.skip(_currentIndex).take(12).toList();
|
|
|
|
List<String> get next12TimeProb => hourlyTime.skip(_currentIndex).take(12).toList();
|
|
List get next12PrecpProb => hourlyPrecpProb.skip(_currentIndex).take(12).toList();
|
|
|
|
|
|
double get maxRain {
|
|
final r = next12Precp.reduce((a, b) => a > b ? a : b);
|
|
return (r < 3) ? 3 : (r * 1.3).ceilToDouble();
|
|
}
|
|
|
|
(int?, int?) getRainPeriod() {
|
|
int? bestStart;
|
|
int? bestEnd;
|
|
int longestLength = 0;
|
|
|
|
int? currentStart;
|
|
|
|
for (int i = 0; i < next12Precp.length; i++) {
|
|
final double precp = next12Precp[i];
|
|
final int prob = next12PrecpProb[i] ?? 0;
|
|
if (precp > 0.2 && prob >= 40) {
|
|
currentStart ??= i;
|
|
} else {
|
|
if (currentStart != null) {
|
|
final length = i - currentStart;
|
|
if (length >= 2 && length > longestLength) {
|
|
bestStart = currentStart;
|
|
bestEnd = i - 1;
|
|
longestLength = length;
|
|
}
|
|
currentStart = null; // reset
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentStart != null) {
|
|
final length = next12Precp.length - currentStart;
|
|
if (length >= 2 && length > longestLength) {
|
|
bestStart = currentStart;
|
|
bestEnd = next12Precp.length - 1;
|
|
}
|
|
}
|
|
|
|
return (bestStart, bestEnd);
|
|
}
|
|
|
|
String _generateTitle(int? start) {
|
|
if (start == null) return "No rain expected";
|
|
|
|
if (start == 0 && next12Precp[0] > 0.2) {
|
|
return willRainStopSoon() ? "Rain will stop soon" : "It's currently raining";
|
|
}
|
|
|
|
final hour = DateTime.parse(next12Time[start]).hour;
|
|
|
|
if (hour >= 0 && hour <= 5) return "Rain expected overnight";
|
|
if (hour >= 6 && hour < 12) return "Rain expected this morning";
|
|
if (hour >= 12 && hour < 17) return "Rain expected this afternoon";
|
|
if (hour >= 17 && hour <= 22) return "Rain expected this evening";
|
|
|
|
return "Rain expected later today";
|
|
}
|
|
|
|
|
|
|
|
bool willRainStopSoon() {
|
|
if (next12Precp[0] <= 0.2 || (next12PrecpProb[0] ?? 0) < 30) return false;
|
|
|
|
int dryCount = 0;
|
|
for (int i = 1; i < next12Precp.length; i++) {
|
|
final double precp = next12Precp[i];
|
|
final int prob = next12PrecpProb[i] ?? 0;
|
|
|
|
if (precp <= 0.2 || prob < 30) {
|
|
dryCount++;
|
|
if (dryCount >= 2) return true;
|
|
} else {
|
|
dryCount = 0;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
Color _barColor(double mm, context) {
|
|
if (mm > 5) return Theme.of(context).colorScheme.error;
|
|
if (mm > 2) return Theme.of(context).colorScheme.tertiary;
|
|
if (mm > 0.2) return Theme.of(context).colorScheme.primary;
|
|
return Theme.of(context).colorScheme.primary;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
|
|
String? generateSummary(int? start, int? end) {
|
|
if (start == null || end == null) return null;
|
|
|
|
final segment = next12Precp.sublist(start, end + 1);
|
|
final max = segment.reduce((a, b) => a > b ? a : b);
|
|
|
|
String label = switch (max) {
|
|
> 5 => "Heavy rain",
|
|
> 2 => "Moderate rain",
|
|
_ => "Light rain"
|
|
};
|
|
|
|
final timeUnit = context.watch<UnitSettingsNotifier>().timeUnit;
|
|
|
|
|
|
final startStr = timeUnit == '24 hr' ? DateFormat.Hm().format(DateTime.parse(next12Time[start])) : DateFormat.jm().format(DateTime.parse(next12Time[start]));
|
|
final endStr = timeUnit == '24 hr' ? DateFormat.Hm().format(DateTime.parse(next12Time[end])) : DateFormat.jm().format(DateTime.parse(next12Time[end]));
|
|
|
|
return "$label from $startStr to $endStr";
|
|
}
|
|
|
|
|
|
final (start, end) = getRainPeriod();
|
|
final title = _generateTitle(start);
|
|
final subtitle = generateSummary(start, end);
|
|
final rain = next12Precp;
|
|
final unitSettings = Provider.of<UnitSettingsNotifier>(context, listen: false);
|
|
final precipitationUnit = unitSettings.precipitationUnit;
|
|
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 0, left: 12, right: 12, top: 0),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Color(selectedContainerBgIndex),
|
|
borderRadius: BorderRadius.circular(18),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.w500)),
|
|
if (subtitle != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 2),
|
|
child: Text(subtitle, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 13)),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
SizedBox(
|
|
height: 90,
|
|
child: BarChart(
|
|
|
|
BarChartData(
|
|
alignment: BarChartAlignment.start,
|
|
maxY: maxRain,
|
|
barTouchData: BarTouchData(enabled: true,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
tooltipBorderRadius: BorderRadius.circular(50),
|
|
getTooltipColor: (group) => Theme.of(context).colorScheme.primaryContainer,
|
|
tooltipPadding: EdgeInsets.only(left: 5, right: 5),
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
final convertedPrecip = precipitationUnit == 'cm'
|
|
? UnitConverter.mmToCm(rod.toY)
|
|
: precipitationUnit == 'in'
|
|
? UnitConverter.mmToIn(rod.toY)
|
|
: rod.toY;
|
|
return BarTooltipItem(
|
|
'${convertedPrecip.toStringAsFixed(1)} $precipitationUnit',
|
|
TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer, fontWeight: FontWeight.w500),
|
|
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
borderData: FlBorderData(
|
|
show: true,
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: Theme.of(context).colorScheme.outlineVariant,
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawHorizontalLine: true,
|
|
|
|
drawVerticalLine: false,
|
|
horizontalInterval: maxRain / 3,
|
|
getDrawingHorizontalLine: (_) => FlLine(
|
|
color: Theme.of(context).colorScheme.outlineVariant,
|
|
strokeWidth: 1,
|
|
),
|
|
|
|
),
|
|
titlesData: FlTitlesData(
|
|
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
|
|
reservedSize: 50,
|
|
getTitlesWidget: (value, _) {
|
|
final roundedMax = maxRain;
|
|
final step = roundedMax / 2;
|
|
final convertedPrecip = precipitationUnit == 'cm'
|
|
? UnitConverter.mmToCm(value)
|
|
: precipitationUnit == 'in'
|
|
? UnitConverter.mmToIn(value)
|
|
: value;
|
|
|
|
if (value == 0 || value == step || value == roundedMax) {
|
|
return Text(
|
|
'${double.parse(convertedPrecip.toStringAsFixed(1))} $precipitationUnit',
|
|
style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
interval: maxRain / 2,
|
|
),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 18,
|
|
|
|
getTitlesWidget: (value, _) {
|
|
final idx = value.toInt();
|
|
if (idx % 3 != 0 || idx >= next12Time.length) return const SizedBox.shrink();
|
|
final dt = DateTime.parse(next12Time[idx]);
|
|
return
|
|
Padding(padding: EdgeInsets.only(left: 8),
|
|
child: Text(
|
|
unitSettings.timeUnit == '24 hr' ? DateFormat.Hm().format(dt) : DateFormat.jm().format(dt),
|
|
|
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 9),
|
|
)
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
barGroups: List.generate(rain.length, (i) {
|
|
return BarChartGroupData(
|
|
x: i,
|
|
barRods: [
|
|
BarChartRodData(
|
|
toY: rain[i],
|
|
width: 15,
|
|
color: _barColor(rain[i], context),
|
|
borderRadius: BorderRadius.only(topLeft: Radius.circular(50), topRight: Radius.circular(50)),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|