diff --git a/lib/data/weather/mapper/location_mapper.dart b/lib/data/weather/mapper/location_mapper.dart new file mode 100644 index 0000000..299d908 --- /dev/null +++ b/lib/data/weather/mapper/location_mapper.dart @@ -0,0 +1,13 @@ +import 'package:weather_app/data/weather/model/location_dto.dart'; +import 'package:weather_app/domain/entity/location.dart'; + +class LocationMapper { + static Location toDomain(LocationDto dto) { + return Location( + latitude: dto.latitude, + longitude: dto.longitude, + country: dto.country, + city: dto.city, + ); + } +} diff --git a/lib/data/weather/mapper/weather_mapper.dart b/lib/data/weather/mapper/weather_mapper.dart new file mode 100644 index 0000000..9e4ea60 --- /dev/null +++ b/lib/data/weather/mapper/weather_mapper.dart @@ -0,0 +1,220 @@ +import 'package:weather_app/data/weather/model/weather_dto.dart'; +import 'package:weather_app/domain/entity/weather.dart'; +import 'package:weather_app/domain/model/temperature.dart'; +import 'package:weather_app/domain/model/current_day_weather_status.dart'; +import 'package:weather_app/domain/model/hourly_status.dart'; +import 'package:weather_app/domain/model/day_forecast.dart'; +import 'package:weather_app/domain/model/speed.dart'; +import 'package:weather_app/domain/model/atmospheric_pressure.dart'; + +class WeatherMapper { + static Weather toDomain(WeatherDto dto) { + return Weather( + currentTemperature: _mapCurrentTemperature(dto), + currentDayWeatherStatus: _mapCurrentDayWeatherStatus(dto), + hourlyStatus: _mapHourlyStatus(dto), + currentDayForecast: _mapCurrentDayForecast(dto), + nextDaysForecast: _mapNextDaysForecast(dto), + isDaytime: _mapIsDaytime(dto), + ); + } + + static Temperature _mapCurrentTemperature(WeatherDto dto) { + final temperature = dto.currentWeather?.temperature ?? 0.0; + final unit = _parseTemperatureUnit(dto.currentUnits?.temperatureUnit); + return Temperature(temperature: temperature, unit: unit); + } + + static TodayWeatherStatus _mapCurrentDayWeatherStatus(WeatherDto dto) { + final windSpeed = Speed( + speed: dto.currentWeather?.windSpeed ?? 0.0, + unit: _parseSpeedUnit(dto.currentUnits?.windSpeedUnit), + ); + + final feelsLike = Temperature( + temperature: dto.currentWeather?.apparentTemperature ?? 0.0, + unit: _parseTemperatureUnit(dto.currentUnits?.apparentTemperatureUnit), + ); + + final pressure = AtmosphericPressure( + pressure: dto.currentWeather?.surfacePressure ?? 0.0, + unit: _parsePressureUnit(dto.currentUnits?.surfacePressureUnit), + ); + + final uvIndex = dto.dailyWeather?.uvIndexMax?.isNotEmpty == true + ? dto.dailyWeather!.uvIndexMax!.first + : 0.0; + + return TodayWeatherStatus( + windSpeed: windSpeed, + humidity: dto.currentWeather?.relativeHumidity ?? 0.0, + rain: dto.currentWeather?.rain ?? 0.0, + uvIndex: uvIndex, + pressure: pressure, + feelsLike: feelsLike, + ); + } + + static List _mapHourlyStatus(WeatherDto dto) { + final hourly = dto.hourly; + if (hourly?.time == null || + hourly?.temperature == null || + hourly?.weatherCode == null) { + return []; + } + + final List hourlyStatuses = []; + final timeList = hourly!.time!; + final tempList = hourly.temperature!; + final codeList = hourly.weatherCode!; + + final maxLength = [ + timeList.length, + tempList.length, + codeList.length, + ].reduce((a, b) => a < b ? a : b); + + for (int i = 0; i < maxLength; i++) { + final timeString = timeList[i]; + final hour = DateTime.parse(timeString).hour; + + final temperature = Temperature( + temperature: tempList[i], + unit: _parseTemperatureUnit(dto.currentUnits?.temperatureUnit), + ); + + hourlyStatuses.add( + HourlyStatus( + hour: hour, + temperature: temperature, + weatherCode: codeList[i], + ), + ); + } + + return hourlyStatuses; + } + + static DayForecast _mapCurrentDayForecast(WeatherDto dto) { + final daily = dto.dailyWeather; + if (daily?.temperatureMin == null || + daily?.temperatureMax == null || + daily?.weatherCode == null) { + return DayForecast( + minTemperature: Temperature( + temperature: 0.0, + unit: TemperatureUnit.celecus, + ), + maxTemperature: Temperature( + temperature: 0.0, + unit: TemperatureUnit.celecus, + ), + weatherConditionCode: 0, + ); + } + + final tempUnit = _parseTemperatureUnit(dto.currentUnits?.temperatureUnit); + + return DayForecast( + minTemperature: Temperature( + temperature: daily!.temperatureMin!.first, + unit: tempUnit, + ), + maxTemperature: Temperature( + temperature: daily.temperatureMax!.first, + unit: tempUnit, + ), + weatherConditionCode: daily.weatherCode!.first, + ); + } + + static List _mapNextDaysForecast(WeatherDto dto) { + final daily = dto.dailyWeather; + if (daily?.temperatureMin == null || + daily?.temperatureMax == null || + daily?.weatherCode == null) { + return []; + } + + final List forecasts = []; + final minTempList = daily!.temperatureMin!; + final maxTempList = daily.temperatureMax!; + final codeList = daily.weatherCode!; + final tempUnit = _parseTemperatureUnit(dto.currentUnits?.temperatureUnit); + + final maxLength = [ + minTempList.length, + maxTempList.length, + codeList.length, + ].reduce((a, b) => a < b ? a : b); + + // Skip the first day (current day) and get the next days + for (int i = 1; i < maxLength; i++) { + forecasts.add( + DayForecast( + minTemperature: Temperature( + temperature: minTempList[i], + unit: tempUnit, + ), + maxTemperature: Temperature( + temperature: maxTempList[i], + unit: tempUnit, + ), + weatherConditionCode: codeList[i], + ), + ); + } + + return forecasts; + } + + static bool _mapIsDaytime(WeatherDto dto) { + return dto.currentWeather?.isDay == 1; + } + + static TemperatureUnit _parseTemperatureUnit(String? unit) { + switch (unit?.toLowerCase()) { + case '°f': + case 'fahrenheit': + return TemperatureUnit.fehrenhite; + case '°c': + case 'celsius': + default: + return TemperatureUnit.celecus; + } + } + + static SpeedUnit _parseSpeedUnit(String? unit) { + switch (unit?.toLowerCase()) { + case 'mph': + case 'mi/h': + return SpeedUnit.mph; + case 'km/h': + case 'kmph': + default: + return SpeedUnit.kmph; + } + } + + static AtmosphericPressureUnit _parsePressureUnit(String? unit) { + switch (unit?.toLowerCase()) { + case 'hpa': + return AtmosphericPressureUnit.hPa; + case 'kpa': + return AtmosphericPressureUnit.kPa; + case 'mbar': + return AtmosphericPressureUnit.mbar; + case 'atm': + return AtmosphericPressureUnit.atm; + case 'psi': + return AtmosphericPressureUnit.psi; + case 'mmhg': + return AtmosphericPressureUnit.mmHg; + case 'inhg': + return AtmosphericPressureUnit.inHg; + case 'pa': + default: + return AtmosphericPressureUnit.pa; + } + } +} diff --git a/lib/data/weather/model/current_units_dto.dart b/lib/data/weather/model/current_units_dto.dart new file mode 100644 index 0000000..054414a --- /dev/null +++ b/lib/data/weather/model/current_units_dto.dart @@ -0,0 +1,31 @@ +class CurrentUnitsDto { + final String? apparentTemperatureUnit; + final String? rainUnit; + final String? relativeHumidityUnit; + final String? surfacePressureUnit; + final String? temperatureUnit; + final String? timeUnit; + final String? windSpeedUnit; + + CurrentUnitsDto({ + this.apparentTemperatureUnit, + this.rainUnit, + this.relativeHumidityUnit, + this.surfacePressureUnit, + this.temperatureUnit, + this.timeUnit, + this.windSpeedUnit, + }); + + factory CurrentUnitsDto.fromJson(Map json) { + return CurrentUnitsDto( + apparentTemperatureUnit: json['apparent_temperature'] as String?, + rainUnit: json['rain'] as String?, + relativeHumidityUnit: json['relative_humidity_2m'] as String?, + surfacePressureUnit: json['surface_pressure'] as String?, + temperatureUnit: json['temperature_2m'] as String?, + timeUnit: json['time'] as String?, + windSpeedUnit: json['wind_speed_10m'] as String?, + ); + } +} diff --git a/lib/data/weather/model/current_weather_dto.dart b/lib/data/weather/model/current_weather_dto.dart new file mode 100644 index 0000000..836e724 --- /dev/null +++ b/lib/data/weather/model/current_weather_dto.dart @@ -0,0 +1,40 @@ +class CurrentWeatherDto { + final double? apparentTemperature; + final double? interval; + final double? rain; + final double? relativeHumidity; + final double? surfacePressure; + final double? temperature; + final String? time; + final int? weatherCode; + final double? windSpeed; + final int? isDay; + + CurrentWeatherDto({ + this.apparentTemperature, + this.interval, + this.rain, + this.relativeHumidity, + this.surfacePressure, + this.temperature, + this.time, + this.weatherCode, + this.windSpeed, + this.isDay, + }); + + factory CurrentWeatherDto.fromJson(Map json) { + return CurrentWeatherDto( + apparentTemperature: (json['apparent_temperature'] as num?)?.toDouble(), + interval: (json['interval'] as num?)?.toDouble(), + rain: (json['rain'] as num?)?.toDouble(), + relativeHumidity: (json['relative_humidity_2m'] as num?)?.toDouble(), + surfacePressure: (json['surface_pressure'] as num?)?.toDouble(), + temperature: (json['temperature_2m'] as num?)?.toDouble(), + time: json['time'] as String?, + weatherCode: json['weather_code'] as int?, + windSpeed: (json['wind_speed_10m'] as num?)?.toDouble(), + isDay: json['is_day'] as int?, + ); + } +} diff --git a/lib/data/weather/model/daily_weather_dto.dart b/lib/data/weather/model/daily_weather_dto.dart new file mode 100644 index 0000000..2fd843b --- /dev/null +++ b/lib/data/weather/model/daily_weather_dto.dart @@ -0,0 +1,32 @@ +class DailyWeatherDto { + final List? temperatureMax; + final List? temperatureMin; + final List? time; + final List? uvIndexMax; + final List? weatherCode; + + DailyWeatherDto({ + this.temperatureMax, + this.temperatureMin, + this.time, + this.uvIndexMax, + this.weatherCode, + }); + + factory DailyWeatherDto.fromJson(Map json) { + return DailyWeatherDto( + temperatureMax: (json['temperature_2m_max'] as List?) + ?.map((e) => (e as num).toDouble()) + .toList(), + temperatureMin: (json['temperature_2m_min'] as List?) + ?.map((e) => (e as num).toDouble()) + .toList(), + time: (json['time'] as List?)?.map((e) => e as String).toList(), + uvIndexMax: (json['uv_index_max'] as List?) + ?.map((e) => (e as num).toDouble()) + .toList(), + weatherCode: + (json['weather_code'] as List?)?.map((e) => e as int).toList(), + ); + } +} diff --git a/lib/data/weather/model/daily_weather_units_dto.dart b/lib/data/weather/model/daily_weather_units_dto.dart new file mode 100644 index 0000000..9caac38 --- /dev/null +++ b/lib/data/weather/model/daily_weather_units_dto.dart @@ -0,0 +1,22 @@ +class DailyWeatherUnitsDto { + final String? temperatureMaxUnit; + final String? temperatureMinUnit; + final String? timeUnit; + final String? uvIndexUnit; + + DailyWeatherUnitsDto({ + this.temperatureMaxUnit, + this.temperatureMinUnit, + this.timeUnit, + this.uvIndexUnit, + }); + + factory DailyWeatherUnitsDto.fromJson(Map json) { + return DailyWeatherUnitsDto( + temperatureMaxUnit: json['temperature_2m_max'] as String?, + temperatureMinUnit: json['temperature_2m_min'] as String?, + timeUnit: json['time'] as String?, + uvIndexUnit: json['uv_index_max'] as String?, + ); + } +} diff --git a/lib/data/weather/model/hourly_weather_dto.dart b/lib/data/weather/model/hourly_weather_dto.dart new file mode 100644 index 0000000..ebc79b4 --- /dev/null +++ b/lib/data/weather/model/hourly_weather_dto.dart @@ -0,0 +1,25 @@ +class HourlyWeatherDto { + final List? temperature; + final List? time; + final List? weatherCode; + + HourlyWeatherDto({ + this.temperature, + this.time, + this.weatherCode, + }); + + factory HourlyWeatherDto.fromJson(Map json) { + return HourlyWeatherDto( + temperature: (json['temperature_2m'] as List?) + ?.map((e) => (e as num).toDouble()) + .toList(), + time: (json['time'] as List?) + ?.map((e) => e as String) + .toList(), + weatherCode: (json['weather_code'] as List?) + ?.map((e) => e as int) + .toList(), + ); + } +} diff --git a/lib/data/weather/model/hourly_weather_units_dto.dart b/lib/data/weather/model/hourly_weather_units_dto.dart new file mode 100644 index 0000000..282f3ef --- /dev/null +++ b/lib/data/weather/model/hourly_weather_units_dto.dart @@ -0,0 +1,16 @@ +class HourlyWeatherUnitsDto { + final String? temperatureUnit; + final String? timeUnit; + + HourlyWeatherUnitsDto({ + this.temperatureUnit, + this.timeUnit, + }); + + factory HourlyWeatherUnitsDto.fromJson(Map json) { + return HourlyWeatherUnitsDto( + temperatureUnit: json['temperature_2m'] as String?, + timeUnit: json['time'] as String?, + ); + } +} diff --git a/lib/data/weather/model/location_dto.dart b/lib/data/weather/model/location_dto.dart new file mode 100644 index 0000000..c04e40c --- /dev/null +++ b/lib/data/weather/model/location_dto.dart @@ -0,0 +1,22 @@ +class LocationDto { + final double latitude; + final double longitude; + final String country; + final String city; + + LocationDto({ + required this.latitude, + required this.longitude, + required this.country, + required this.city, + }); + + static Future fromJson(Map json) async { + return LocationDto( + latitude: (json['lat'])?.toDouble(), + longitude: (json['lon'])?.toDouble(), + country: (json['country']), + city: (json['city']), + ); + } +} diff --git a/lib/data/weather/model/weather_dto.dart b/lib/data/weather/model/weather_dto.dart new file mode 100644 index 0000000..adff982 --- /dev/null +++ b/lib/data/weather/model/weather_dto.dart @@ -0,0 +1,47 @@ +import 'current_weather_dto.dart'; +import 'current_units_dto.dart'; +import 'daily_weather_dto.dart'; +import 'daily_weather_units_dto.dart'; +import 'hourly_weather_dto.dart'; +import 'hourly_weather_units_dto.dart'; + +class WeatherDto { + final CurrentWeatherDto? currentWeather; + final CurrentUnitsDto? currentUnits; + final DailyWeatherDto? dailyWeather; + final DailyWeatherUnitsDto? dailyWeatherUnits; + final HourlyWeatherDto? hourly; + final HourlyWeatherUnitsDto? hourlyWeatherUnits; + + WeatherDto({ + this.currentWeather, + this.currentUnits, + this.dailyWeather, + this.dailyWeatherUnits, + this.hourly, + this.hourlyWeatherUnits, + }); + + factory WeatherDto.fromJson(Map json) { + return WeatherDto( + currentWeather: json['current'] != null + ? CurrentWeatherDto.fromJson(json['current']) + : null, + currentUnits: json['current_units'] != null + ? CurrentUnitsDto.fromJson(json['current_units']) + : null, + dailyWeather: json['daily'] != null + ? DailyWeatherDto.fromJson(json['daily']) + : null, + dailyWeatherUnits: json['daily_units'] != null + ? DailyWeatherUnitsDto.fromJson(json['daily_units']) + : null, + hourly: json['hourly'] != null + ? HourlyWeatherDto.fromJson(json['hourly']) + : null, + hourlyWeatherUnits: json['hourly_units'] != null + ? HourlyWeatherUnitsDto.fromJson(json['hourly_units']) + : null, + ); + } +} diff --git a/lib/data/weather/repository/location_repository_impl.dart b/lib/data/weather/repository/location_repository_impl.dart new file mode 100644 index 0000000..0c3ea73 --- /dev/null +++ b/lib/data/weather/repository/location_repository_impl.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; +import 'package:weather_app/data/weather/mapper/location_mapper.dart'; +import 'package:weather_app/data/weather/model/location_dto.dart'; +import 'package:weather_app/domain/entity/location.dart'; + +import '../../../domain/repository/location_repository.dart'; + +class LocationRepositoryImpl extends LocationRepository { + final Dio dio; + + LocationRepositoryImpl(this.dio); + + @override + Future getCurrentLocation() async { + try { + final response = await dio.get( + "http://ip-api.com/json/", + queryParameters: { + 'fields': + 'status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,org,as,query', + }, + ); + + if (response.statusCode == 200 && response.data != null) { + return LocationMapper.toDomain( + await LocationDto.fromJson(response.data), + ); + } else { + throw Exception( + 'Failed to fetch location. Status: ${response.statusCode}', + ); + } + } on DioException catch (e) { + throw Exception('Failed to fetch location: ${e.message}'); + } + } +} diff --git a/lib/data/weather/repository/weather_repository_impl.dart b/lib/data/weather/repository/weather_repository_impl.dart new file mode 100644 index 0000000..447f328 --- /dev/null +++ b/lib/data/weather/repository/weather_repository_impl.dart @@ -0,0 +1,41 @@ +import 'package:dio/dio.dart'; +import 'package:weather_app/data/weather/mapper/weather_mapper.dart'; +import 'package:weather_app/data/weather/model/weather_dto.dart'; +import 'package:weather_app/domain/entity/weather.dart'; +import 'package:weather_app/domain/repository/weather_repository.dart'; + +const String openMeteoApi = 'https://api.open-meteo.com/v1/forecast'; + +class WeatherRepositoryImpl implements WeatherRepository { + final Dio dio; + + WeatherRepositoryImpl(this.dio); + + @override + Future getWeatherForecast(double latitude, double longitude) async { + try { + final response = await dio.get( + openMeteoApi, + queryParameters: { + 'latitude': latitude, + 'longitude': longitude, + 'daily': + 'temperature_2m_max,temperature_2m_min,uv_index_max,weather_code', + 'hourly': 'temperature_2m,weather_code', + 'current': + 'weather_code,relative_humidity_2m,wind_speed_10m,rain,surface_pressure,apparent_temperature,temperature_2m,is_day', + }, + ); + + if (response.statusCode == 200 && response.data != null) { + return WeatherMapper.toDomain(WeatherDto.fromJson(response.data)); + } else { + throw Exception( + 'Failed to fetch weather data. Status: ${response.statusCode}', + ); + } + } on DioException catch (e) { + throw Exception('Failed to fetch weather data: ${e.message}'); + } + } +} diff --git a/lib/domain/entity/location.dart b/lib/domain/entity/location.dart index 880324e..b7f61c1 100644 --- a/lib/domain/entity/location.dart +++ b/lib/domain/entity/location.dart @@ -1,8 +1,15 @@ import '../model/location_coordinate.dart'; class Location { - final LocationCoordinate coordinate; - final String cityName; + final double latitude; + final double longitude; + final String country; + final String city; - Location({required this.coordinate, required this.cityName}); + Location({ + required this.latitude, + required this.longitude, + required this.country, + required this.city, + }); } diff --git a/lib/domain/repository/location_repository.dart b/lib/domain/repository/location_repository.dart index f5cc7ee..23453dc 100644 --- a/lib/domain/repository/location_repository.dart +++ b/lib/domain/repository/location_repository.dart @@ -1,5 +1,5 @@ import 'package:weather_app/domain/entity/location.dart'; -abstract interface class LocationRepository { - Future getCurrentLocation(); -} \ No newline at end of file +abstract class LocationRepository { + Future getCurrentLocation(); +} diff --git a/lib/domain/repository/weather_repository.dart b/lib/domain/repository/weather_repository.dart index b397f54..78d871c 100644 --- a/lib/domain/repository/weather_repository.dart +++ b/lib/domain/repository/weather_repository.dart @@ -1,6 +1,6 @@ import 'package:weather_app/domain/entity/weather.dart'; import '../model/location_coordinate.dart'; -abstract interface class WeatherRepository { - Future getWeatherForecast(LocationCoordinate locationCoordinate); -} \ No newline at end of file +abstract class WeatherRepository { + Future getWeatherForecast(double latitude, double longitude); +} diff --git a/pubspec.lock b/pubspec.lock index e3d99d9..3500076 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,6 +193,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: @@ -356,13 +372,21 @@ packages: source: hosted version: "1.0.5" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted version: "4.9.0" + json_serializable: + dependency: "direct main" + description: + name: json_serializable + sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe" + url: "https://pub.dev" + source: hosted + version: "6.11.1" leak_tracker: dependency: transitive description: @@ -536,6 +560,22 @@ packages: description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "9098ab86015c4f1d8af6486b547b11100e73b193e1899015033cb3e14ad20243" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" + url: "https://pub.dev" + source: hosted + version: "1.3.8" source_span: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 74ae339..1cb12c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: get_it: ^8.2.0 bloc: ^9.1.0 flutter_bloc: ^9.1.1 + dio: ^5.6.0 dev_dependencies: flutter_test: