Skip to content

Commit

Permalink
Merge pull request #2367 from airqo-platform/forecast-fix
Browse files Browse the repository at this point in the history
Add air quality forecasting functionality and improve analytics compo…
  • Loading branch information
Baalmart authored Jan 8, 2025
2 parents 270df7f + 7a6252f commit a88263d
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 115 deletions.
57 changes: 47 additions & 10 deletions mobile-v3/lib/src/app/dashboard/models/forecast_response.dart
Original file line number Diff line number Diff line change
@@ -1,41 +1,78 @@
import 'package:airqo/src/app/dashboard/models/airquality_response.dart';
class ForecastResponse {
Map<String, AqiRange> aqiRanges;
List<Forecast> forecasts;

ForecastResponse({
required this.aqiRanges,
required this.forecasts,
});

factory ForecastResponse.fromJson(Map<String, dynamic> json) =>
ForecastResponse(
forecasts: List<Forecast>.from(
json["forecasts"].map((x) => Forecast.fromJson(x))),
aqiRanges: Map<String, AqiRange>.from(json["aqi_ranges"].map((key, value) => MapEntry(key, AqiRange.fromJson(value)))),

forecasts: List<Forecast>.from(json["forecasts"].map((x) => Forecast.fromJson(x))),
);

Map<String, dynamic> toJson() => {
"forecasts": List<dynamic>.from(forecasts.map((x) => x.toJson())),
};
}

class AqiRange {
final String aqiCategory;
final String aqiColor;
final String aqiColorName;
final String label;
final double? max;
final double min;

AqiRange({
required this.aqiCategory,
required this.aqiColor,
required this.aqiColorName,
required this.label,
required this.max,
required this.min,
});

factory AqiRange.fromJson(Map<String, dynamic> json) {
return AqiRange(
aqiCategory: json['aqi_category'],
aqiColor: json['aqi_color'],
aqiColorName: json['aqi_color_name'],
label: json['label'],
max: json['max'] != null ? json['max'].toDouble() : null,
min: json['min'].toDouble(),
);
}
}

class Forecast {
Measurement measurement;
double pm25;
DateTime time;
final String aqiCategory;
final String aqiColor;
final String aqiColorName;
final double pm25;
final DateTime time;

Forecast({
required this.measurement,
required this.aqiCategory,
required this.aqiColor,
required this.aqiColorName,
required this.pm25,
required this.time,
});

factory Forecast.fromJson(Map<String, dynamic> json) => Forecast(
measurement: Measurement.fromJson(json["measurement"]),
pm25: json["pm2_5"]?.toDouble(),
aqiCategory: json["aqi_category"],
aqiColor: json["aqi_color"],
aqiColorName: json["aqi_color_name"],
pm25: json["pm2_5"]?.toDouble(),
time: DateTime.parse(json["time"]),
);

Map<String, dynamic> toJson() => {
"pm2_5": pm25,
"time": time.toIso8601String(),
};
}
}
3 changes: 1 addition & 2 deletions mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'package:airqo/src/app/dashboard/widgets/countries_chip.dart';
import 'package:airqo/src/app/dashboard/widgets/dashboard_loading.dart';
import 'package:airqo/src/app/other/theme/bloc/theme_bloc.dart';
import 'package:airqo/src/app/profile/bloc/user_bloc.dart';
import 'package:airqo/src/app/profile/pages/profile_page.dart';
import 'package:airqo/src/app/shared/pages/error_page.dart';
import 'package:airqo/src/app/shared/widgets/loading_widget.dart';
import 'package:airqo/src/app/shared/widgets/page_padding.dart';
Expand Down Expand Up @@ -308,4 +307,4 @@ class CountryModel {
final String countryName;

const CountryModel(this.flag, this.countryName);
}
}
4 changes: 2 additions & 2 deletions mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class AnalyticsCard extends StatelessWidget {
SizedBox(
child: Center(
child: SvgPicture.asset(
getAirQualityIcon(measurement, measurement.pm25!.value ?? 10),
getAirQualityIcon(measurement, measurement.pm25!.value!),
height: 96,
width: 96,
),
Expand Down Expand Up @@ -110,4 +110,4 @@ class AnalyticsCard extends StatelessWidget {
),
);
}
}
}
190 changes: 95 additions & 95 deletions mobile-v3/lib/src/app/dashboard/widgets/analytics_forecast_widget.dart
Original file line number Diff line number Diff line change
@@ -1,103 +1,103 @@
// import 'package:airqo/src/app/dashboard/bloc/forecast/forecast_bloc.dart';
// import 'package:airqo/src/app/shared/widgets/loading_widget.dart';
// import 'package:airqo/src/meta/utils/colors.dart';
// import 'package:airqo/src/meta/utils/utils.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_bloc/flutter_bloc.dart';
// import 'package:flutter_svg/flutter_svg.dart';
// import 'package:intl/intl.dart';
import 'package:airqo/src/app/dashboard/bloc/forecast/forecast_bloc.dart';
import 'package:airqo/src/app/shared/widgets/loading_widget.dart';
import 'package:airqo/src/meta/utils/colors.dart';
import 'package:airqo/src/meta/utils/forecast_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';

// class AnalyticsForecastWidget extends StatefulWidget {
// final String siteId;
// const AnalyticsForecastWidget({super.key, required this.siteId});
class AnalyticsForecastWidget extends StatefulWidget {
final String siteId;
const AnalyticsForecastWidget({super.key, required this.siteId});

// @override
// State<AnalyticsForecastWidget> createState() =>
// _AnalyticsForecastWidgetState();
// }
@override
State<AnalyticsForecastWidget> createState() =>
_AnalyticsForecastWidgetState();
}

// class _AnalyticsForecastWidgetState extends State<AnalyticsForecastWidget> {
// ForecastBloc? forecastBloc;
class _AnalyticsForecastWidgetState extends State<AnalyticsForecastWidget> {
ForecastBloc? forecastBloc;

// @override
// void initState() {
// forecastBloc = context.read<ForecastBloc>()
// ..add(LoadForecast(widget.siteId));
// super.initState();
// }
@override
void initState() {
forecastBloc = context.read<ForecastBloc>()
..add(LoadForecast(widget.siteId));
super.initState();
}

// @override
// Widget build(BuildContext context) {
// return BlocBuilder<ForecastBloc, ForecastState>(
// builder: (context, state) {
// if (state is ForecastLoaded) {
// return Row(
// children: state.response.forecasts
// .map((e) => ForeCastChip(
// active: false,
// date: DateFormat.d().format(e.time),
// day: DateFormat.E().format(e.time)[0],
// imagePath: getAirQualityIcon(e.measurement, e.pm25),
// ))
// .toList());
// } else if (state is ForecastLoading) {
// return Row(
// mainAxisAlignment: MainAxisAlignment.spaceAround,
// children: List.generate(7, (index) {
// return ShimmerContainer(
// height: 47 + 45, borderRadius: 22, width: 40);
// }));
// }
@override
Widget build(BuildContext context) {
return BlocBuilder<ForecastBloc, ForecastState>(
builder: (context, state) {
if (state is ForecastLoaded) {
return Row(
children: state.response.forecasts
.map((e) => ForeCastChip(
active: false,
date: DateFormat.d().format(e.time),
day: DateFormat.E().format(e.time)[0],
imagePath: getForecastAirQualityIcon(e.pm25, state.response.aqiRanges),
))
.toList());
} else if (state is ForecastLoading) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(7, (index) {
return ShimmerContainer(
height: 47 + 45, borderRadius: 22, width: 40);
}));
}

// return Container(
// child: Center(
// child: Text(state.toString()),
// ),
// );
// },
// );
// }
// }
return Container(
child: Center(
child: Text(state.toString()),
),
);
},
);
}
}

// class ForeCastChip extends StatelessWidget {
// final bool active;
// final String day;
// final String imagePath;
// final String date;
// const ForeCastChip(
// {super.key,
// required this.active,
// required this.imagePath,
// required this.date,
// required this.day});
class ForeCastChip extends StatelessWidget {
final bool active;
final String day;
final String imagePath;
final String date;
const ForeCastChip(
{super.key,
required this.active,
required this.imagePath,
required this.date,
required this.day});

// @override
// Widget build(BuildContext context) {
// return Expanded(
// child: Container(
// decoration: BoxDecoration(
// color: active
// ? AppColors.primaryColor
// : Theme.of(context).highlightColor,
// borderRadius: BorderRadius.circular(22)),
// padding: const EdgeInsets.symmetric(vertical: 8),
// margin: const EdgeInsets.symmetric(horizontal: 5),
// height: 47 + 45,
// child: Column(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(day),
// Text(date),
// SizedBox(
// child: Center(
// child: SvgPicture.asset(
// imagePath,
// height: 26,
// width: 26,
// ),
// ),
// ),
// ])),
// );
// }
// }
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: active
? AppColors.primaryColor
: Theme.of(context).highlightColor,
borderRadius: BorderRadius.circular(22)),
padding: const EdgeInsets.symmetric(vertical: 8),
margin: const EdgeInsets.symmetric(horizontal: 5),
height: 47 + 45,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(day),
Text(date),
SizedBox(
child: Center(
child: SvgPicture.asset(
imagePath,
height: 26,
width: 26,
),
),
),
])),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:airqo/src/app/dashboard/widgets/analytics_card.dart';
import 'package:airqo/src/app/dashboard/widgets/analytics_forecast_widget.dart';
import 'package:airqo/src/meta/utils/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

class AnalyticsSpecifics extends StatefulWidget {
final Measurement measurement;
Expand Down Expand Up @@ -104,9 +103,9 @@ class _AnalyticsSpecificsState extends State<AnalyticsSpecifics> {
],
),
SizedBox(height: 16),
// AnalyticsForecastWidget(
// siteId: widget.measurement.siteDetails!.id!,
// ),
AnalyticsForecastWidget(
siteId: widget.measurement.siteDetails!.id!,
),
SizedBox(height: 16),
],
),
Expand Down Expand Up @@ -185,4 +184,4 @@ class _AnalyticsSpecificsState extends State<AnalyticsSpecifics> {
),
);
}
}
}
40 changes: 40 additions & 0 deletions mobile-v3/lib/src/meta/utils/forecast_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:airqo/src/app/dashboard/models/forecast_response.dart';

String getForecastAirQualityIcon(double value, Map<String, AqiRange> aqiRanges) {
switch (getAirQuality(value, aqiRanges)) {
case "Good":
return "assets/images/shared/airquality_indicators/good.svg";

case "Moderate":
return "assets/images/shared/airquality_indicators/moderate.svg";

case "Unhealthy":
return "assets/images/shared/airquality_indicators/unhealthy.svg";

case "Unhealthy for Sensitive Groups":
return "assets/images/shared/airquality_indicators/unhealthy-sensitive.svg";

case "Very Unhealthy":
return "assets/images/shared/airquality_indicators/very-unhealthy.svg";

case "Hazardous":
return "assets/images/shared/airquality_indicators/hazardous.svg";


case "Unavailable":
return "assets/images/shared/airquality_indicators/unavailable.svg";


default:
return "";
}
}

String getAirQuality(double value, Map<String, AqiRange> aqiRanges) {
for (var range in aqiRanges.values) {
if (value >= range.min && (range.max == null || value <= range.max!)) {
return range.aqiCategory;
}
}
return "Unavailable";
}
2 changes: 1 addition & 1 deletion mobile-v3/lib/src/meta/utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ String _getDynamicAirQuality(AqiRanges aqiRanges, double value) {
}

return "Unavailable";
}
}

0 comments on commit a88263d

Please sign in to comment.