Skip to content

Commit

Permalink
Add air quality forecasting functionality and improve analytics compo…
Browse files Browse the repository at this point in the history
…nents
Mozart299 committed Jan 8, 2025
1 parent 270df7f commit 7a6252f
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
@@ -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';
@@ -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
@@ -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,
),
@@ -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
@@ -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;
@@ -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),
],
),
@@ -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
@@ -70,4 +70,4 @@ String _getDynamicAirQuality(AqiRanges aqiRanges, double value) {
}

return "Unavailable";
}
}

0 comments on commit 7a6252f

Please sign in to comment.