Icons, minor improvements, refactoring of weather providers

This commit is contained in:
2025-01-31 21:15:20 +01:00
parent 6f0eb478cc
commit f4e79a544d
26 changed files with 676 additions and 230 deletions
@@ -1,29 +1,36 @@
package eur.one00kb.minipogodynka;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.app.TabActivity;
import android.content.Intent;
import android.location.Address;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ExpandableListView;
import android.widget.ListView;
import java.util.ArrayList;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import eur.one00kb.minipogodynka.adapters.DetailsAdapter;
import eur.one00kb.minipogodynka.adapters.HourlyAdapter;
import eur.one00kb.minipogodynka.weather.WeatherAPIFetcher;
import eur.one00kb.minipogodynka.weather.WeatherAPIParser;
import eur.one00kb.minipogodynka.views.NowView;
import eur.one00kb.minipogodynka.weather.MetNoProvider;
import eur.one00kb.minipogodynka.weather.MeteoFranceProvider;
import eur.one00kb.minipogodynka.weather.OpenMeteoProvider;
import eur.one00kb.minipogodynka.weather.WeatherData;
import eur.one00kb.minipogodynka.weather.WeatherDataProvider;
// TODO: Location history
// TODO: Replace Date with Calendar, apparently it's better
// TODO: Action icons
// TODO: Make the refresh button spin instead of showing a dialog
// TODO: Make the adapters show "unavailable" when data is unavailable, which happens sometimes
// with met.no and Meteo France
/**
* @noinspection deprecation
@@ -33,23 +40,35 @@ public class MainActivity extends TabActivity {
private PreferenceHolder mPreferenceHolder;
private Address mCurrentAddress;
private Map<String, WeatherDataProvider> mWeatherDataProviders;
private Map<Date, List<WeatherData>> mWeatherData;
private NowView mNowView;
private HourlyAdapter mHourlyAdapter;
private DetailsAdapter mDetailsAdapter;
private ExpandableListView mHourlyView;
private AlertDialog mLocationSearchDialog;
private AlertDialog mDetailsDialog;
private ProgressDialog mProgressDialog;
private AlertDialog mInfoDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPreferenceHolder = new PreferenceHolder(this);
mWeatherDataProviders = new HashMap<>();
mWeatherDataProviders.put("om", new OpenMeteoProvider(this));
mWeatherDataProviders.put("met", new MetNoProvider(this));
mWeatherDataProviders.put("mf", new MeteoFranceProvider(this));
mHourlyAdapter = new HourlyAdapter(this);
mDetailsAdapter = new DetailsAdapter(this);
mNowView = (NowView) getLayoutInflater().inflate(R.layout.view_now, null);
mLocationSearchDialog = new AlertDialog.Builder(this)
.setCancelable(false)
.setNegativeButton(android.R.string.cancel, null)
@@ -63,6 +82,11 @@ public class MainActivity extends TabActivity {
.setAdapter(mDetailsAdapter, null)
.create();
mProgressDialog = new ProgressDialog(this);
mInfoDialog = new AlertDialog.Builder(this)
.setPositiveButton(android.R.string.ok, null)
.create();
mHourlyView = new ExpandableListView(this);
mHourlyView.setAdapter(mHourlyAdapter);
@@ -82,9 +106,10 @@ public class MainActivity extends TabActivity {
return false;
});
getTabHost().addTab(getTabHost().newTabSpec("now")
.setIndicator("Now").setContent((tag) -> mNowView));
getTabHost().addTab(getTabHost().newTabSpec("hourly")
.setIndicator("Hourly")
.setContent((tag) -> mHourlyView));
.setIndicator("Hourly").setContent((tag) -> mHourlyView));
setDefaultTab(0);
}
@@ -99,7 +124,7 @@ public class MainActivity extends TabActivity {
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_update) {
if (id == R.id.action_refresh) {
fetchApiData(this::updateInfo);
} else if (id == R.id.action_location) {
mLocationSearchDialog.show();
@@ -118,21 +143,34 @@ public class MainActivity extends TabActivity {
}
private void updateInfo() {
mHourlyAdapter.setKeysAndData(new ArrayList<>(mWeatherData.keySet()), mWeatherData);
mHourlyAdapter.setData(mWeatherData);
mHourlyAdapter.notifyDataSetChanged();
mHourlyView.expandGroup(0);
mProgressDialog.dismiss();
}
private void fetchApiData(Runnable afterCallback) {
if (mCurrentAddress == null) {
mInfoDialog.setTitle("No location selected");
mInfoDialog.setMessage("Before refreshing, please select a location");
mInfoDialog.show();
return;
}
Utilities.quickToast(this, "Fetching weather data…");
mProgressDialog.setMessage("Fetching weather data…");
mProgressDialog.show();
new Thread(() -> {
String apiData = WeatherAPIFetcher.fetch(mCurrentAddress.getLatitude(),
mCurrentAddress.getLongitude(), mPreferenceHolder.getProvider());
mWeatherData = WeatherAPIParser.parse(apiData, mPreferenceHolder.getProvider());
try {
mWeatherData = mWeatherDataProviders.get(mPreferenceHolder.getProvider())
.provide(mCurrentAddress.getLatitude(), mCurrentAddress.getLongitude());
} catch (IOException e) {
Log.e(TAG, "Error getting data from weather provider", e);
mInfoDialog.setTitle("Error while getting data from weather provider");
mInfoDialog.setMessage(e.getMessage());
mInfoDialog.show();
}
runOnUiThread(afterCallback);
}).start();
}
@@ -5,7 +5,7 @@ import android.widget.Toast;
import java.util.Date;
public class Utilities {
public class Utils {
public static Date getDayOnly() {
Date date = new Date();
date.setHours(0);
@@ -1,24 +1,26 @@
package eur.one00kb.minipogodynka.adapters;
import android.content.Context;
import android.text.Layout;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eur.one00kb.minipogodynka.R;
import eur.one00kb.minipogodynka.weather.WeatherData;
import eur.one00kb.minipogodynka.weather.WeatherUtils;
public class HourlyAdapter extends BaseExpandableListAdapter {
private Context mContext;
private final Context mContext;
private final List<Date> mKeys;
private final Map<Date, List<WeatherData>> mData;
@@ -28,10 +30,10 @@ public class HourlyAdapter extends BaseExpandableListAdapter {
mData = new LinkedHashMap<>();
}
public void setKeysAndData(List<Date> keys, Map<Date, List<WeatherData>> data) {
public void setData(Map<Date, List<WeatherData>> data) {
mKeys.clear();
mData.clear();
mKeys.addAll(keys);
mKeys.addAll(new ArrayList<>(data.keySet()));
mData.putAll(data);
}
@@ -73,12 +75,12 @@ public class HourlyAdapter extends BaseExpandableListAdapter {
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext)
.inflate(R.layout.item_group, null);
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_group, null);
}
((TextView) convertView).setText(String.format("%tA, %<td",
(Date) getGroup(groupPosition)));
((TextView) convertView)
.setText(String.format(Locale.getDefault(), "%tA, %<te %<tB",
(Date) getGroup(groupPosition)));
return convertView;
}
@@ -94,9 +96,13 @@ public class HourlyAdapter extends BaseExpandableListAdapter {
((TextView) convertView.findViewById(R.id.hour_time))
.setText(String.format("%tR", child.date));
((TextView) convertView.findViewById(R.id.hour_temp))
.setText(String.format("%.0f °C", child.temperature));
.setText(String.format("%d °C", Math.round(child.temperature)));
((TextView) convertView.findViewById(R.id.hour_precp))
.setText(String.format("%.0f %%", child.precipitation));
.setText(String.format("%d mm", Math.round(child.precipitationMm)));
((ImageView) convertView.findViewById(R.id.hour_icon))
.setImageDrawable(mContext.getResources()
.getDrawable(WeatherUtils.getDrawableforCode(child.getWeatherCode())));
return convertView;
}
@@ -16,6 +16,7 @@ import android.widget.AutoCompleteTextView;
import android.widget.LinearLayout;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -36,6 +37,8 @@ public class LocationSearchView extends LinearLayout
private final Geocoder mGeocoder;
private final Handler mGeocodingHandler;
private final AddressAdapter mAddressAdapter;
private AutoCompleteTextView mSearchField;
private final List<Address> mAddressHistory;
private boolean mIsGeocodingAvailable;
public LocationSearchView(Context context, AttributeSet attrs) {
@@ -44,6 +47,7 @@ public class LocationSearchView extends LinearLayout
mGeocodingHandler = new Handler(Looper.myLooper());
mAddressAdapter = new AddressAdapter(getContext());
mAddressAdapter.setNotifyOnChange(false);
mAddressHistory = new ArrayList<>();
mIsGeocodingAvailable = true;
mGeocoder = new Geocoder(getContext(), Locale.getDefault());
@@ -61,7 +65,7 @@ public class LocationSearchView extends LinearLayout
protected void onFinishInflate() {
super.onFinishInflate();
AutoCompleteTextView mSearchField = findViewById(R.id.dl_search);
mSearchField = findViewById(R.id.dl_search);
if (mIsGeocodingAvailable) {
mSearchField.setAdapter(mAddressAdapter);
@@ -93,6 +97,8 @@ public class LocationSearchView extends LinearLayout
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mAddressHistory.add(mAddressAdapter.getItem(position));
mSearchField.clearFocus();
((MainActivity) getContext()).setAddress(mAddressAdapter.getItem(position));
AutoCompleteTextView mSearchField = findViewById(R.id.dl_search);
mSearchField.setText("");
@@ -0,0 +1,16 @@
package eur.one00kb.minipogodynka.views;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
public class NowView extends RelativeLayout {
public NowView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
}
}
@@ -0,0 +1,91 @@
package eur.one00kb.minipogodynka.weather;
import android.content.Context;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eur.one00kb.minipogodynka.Utils;
public class MetNoProvider extends WeatherDataProvider {
private static final String TAG = "MetNoProvider";
private static final DateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
public MetNoProvider(Context context) {
super(context);
setAPILinkFormat("https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=%f&lon=%f");
}
@Override
public Map<Date, List<WeatherData>> parseData(String data) {
Map<Date, List<WeatherData>> weatherDays = new LinkedHashMap<>();
try {
JSONObject root = new JSONObject(data);
JSONArray timeseries = root.getJSONObject("properties")
.getJSONArray("timeseries");
Date currentDate =
DATE_FORMAT.parse(timeseries.getJSONObject(0).getString("time"));
List<WeatherData> dayData = new ArrayList<>();
for (int i = 0; i < timeseries.length(); ++i) {
JSONObject obj = timeseries.getJSONObject(i);
Date date = DATE_FORMAT.parse(obj.getString("time"));
if (date.getDay() != currentDate.getDay()) {
weatherDays.put(Utils.toDayOnly(currentDate), dayData);
dayData = new ArrayList<>();
}
JSONObject instant = obj.getJSONObject("data")
.getJSONObject("instant").getJSONObject("details");
WeatherData hourData = new WeatherData();
hourData.date = date;
hourData.temperature = instant.getDouble("air_temperature");
hourData.humidity = instant.getDouble("relative_humidity");
hourData.pressure = instant.getDouble("air_pressure_at_sea_level");
hourData.cloudCover = instant.getDouble("cloud_area_fraction");
hourData.windSpeed = instant.getDouble("wind_speed");
hourData.windDirection = instant.getDouble("wind_from_direction");
try {
JSONObject next1hr = obj.getJSONObject("data")
.getJSONObject("next_1_hours").getJSONObject("details");
// I think this should be correct. If the time is 13, then it's the data for the
// next 60 minutes, isn't it?
hourData.precipitationMm = next1hr.getDouble("precipitation_amount");
} catch (JSONException e) {
Log.i(TAG, "met.no provides no next hour values for this time");
}
try {
hourData.uvIndex = instant.getDouble("ultraviolet_index_clear_sky");
} catch (JSONException e) {
Log.i(TAG, "met.no provides no UV index values for this time");
}
currentDate = date;
dayData.add(hourData);
}
} catch (JSONException | ParseException e) {
Log.e(TAG, "Error when parsing weather data", e);
}
return weatherDays;
}
}
@@ -0,0 +1,76 @@
package eur.one00kb.minipogodynka.weather;
import android.content.Context;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eur.one00kb.minipogodynka.Utils;
public class MeteoFranceProvider extends WeatherDataProvider {
private static final String TAG = "MeteoFranceProvider";
// Not mine. Taken off GitHub.
private static final String API_TOKEN = "__Wj7dVSTjV9YGu1guveLyDq0g7S7TfTjaHBTPTpO0kj8__";
private static final DateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
public MeteoFranceProvider(Context context) {
super(context);
setAPILinkFormat("https://webservice.meteofrance.com/v2/forecast?lat=%f&lon=%f&hourly=true&lang=en&token=" + API_TOKEN);
}
@Override
public Map<Date, List<WeatherData>> parseData(String data) {
Map<Date, List<WeatherData>> weatherDays = new LinkedHashMap<>();
try {
JSONObject root = new JSONObject(data);
JSONArray hourly = root.getJSONObject("properties").getJSONArray("forecast");
Date currentDate = DATE_FORMAT.parse(hourly.getJSONObject(0).getString("time"));
List<WeatherData> dayData = new ArrayList<>();
for (int i = 0; i < hourly.length(); ++i) {
JSONObject hour = hourly.getJSONObject(i);
Date date = DATE_FORMAT.parse(hour.getString("time"));
if (date.getDay() != currentDate.getDay()) {
weatherDays.put(Utils.toDayOnly(currentDate), dayData);
dayData = new ArrayList<>();
}
WeatherData hourData = new WeatherData();
hourData.date = date;
hourData.temperature = hour.getDouble("T");
hourData.humidity = hour.getDouble("relative_humidity");
hourData.precipitationMm = hour.getDouble("rain_1h");
// No precipitation percent with Meteo France.
hourData.pressure = hour.getDouble("P_sea");
hourData.cloudCover = hour.getDouble("total_cloud_cover");
hourData.windSpeed = hour.getDouble("wind_speed");
hourData.windDirection = hour.getDouble("wind_direction");
// And no UV index data, either.
currentDate = date;
dayData.add(hourData);
}
} catch (JSONException | ParseException e) {
Log.e(TAG, "Error when parsing weather data", e);
}
return weatherDays;
}
}
@@ -0,0 +1,83 @@
package eur.one00kb.minipogodynka.weather;
import android.content.Context;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eur.one00kb.minipogodynka.Utils;
public class OpenMeteoProvider extends WeatherDataProvider {
private static final String TAG = "OpenMeteoProvider";
private static final DateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.US);
public OpenMeteoProvider(Context context) {
super(context);
setAPILinkFormat("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f&hourly=temperature_2m,relative_humidity_2m,precipitation,pressure_msl,cloud_cover,wind_speed_10m,wind_direction_10m,uv_index&wind_speed_unit=ms&forecast_days=16");
}
@Override
public Map<Date, List<WeatherData>> parseData(String data) {
Map<Date, List<WeatherData>> weatherDays = new LinkedHashMap<>();
try {
JSONObject root = new JSONObject(data);
JSONObject hourly = root.getJSONObject("hourly");
JSONArray times = hourly.getJSONArray("time");
JSONArray temperatures = hourly.getJSONArray("temperature_2m");
JSONArray humidities = hourly.getJSONArray("relative_humidity_2m");
JSONArray precipitations = hourly.getJSONArray("precipitation");
JSONArray pressures = hourly.getJSONArray("pressure_msl");
JSONArray cloudCovers = hourly.getJSONArray("cloud_cover");
JSONArray windSpeeds = hourly.getJSONArray("wind_speed_10m");
JSONArray windDirections = hourly.getJSONArray("wind_direction_10m");
JSONArray uvIndexes = hourly.getJSONArray("uv_index");
Date currentDate = DATE_FORMAT.parse(times.getString(0));
List<WeatherData> dayData = new ArrayList<>();
for (int i = 0; i < times.length(); ++i) {
Date date = DATE_FORMAT.parse(times.getString(i));
if (date.getDay() != currentDate.getDay()) {
weatherDays.put(Utils.toDayOnly(currentDate), dayData);
dayData = new ArrayList<>();
}
WeatherData hourData = new WeatherData();
hourData.date = date;
hourData.temperature = temperatures.getDouble(i);
hourData.humidity = humidities.getDouble(i);
hourData.precipitationMm = precipitations.getDouble(i);
hourData.pressure = pressures.getDouble(i);
hourData.cloudCover = cloudCovers.getDouble(i);
hourData.windSpeed = windSpeeds.getDouble(i);
hourData.windDirection = windDirections.getDouble(i);
hourData.uvIndex = uvIndexes.getDouble(i);
dayData.add(hourData);
currentDate = date;
}
weatherDays.put(currentDate, dayData);
} catch (JSONException | ParseException e) {
Log.e(TAG, "Error when parsing weather data", e);
}
return weatherDays;
}
}
@@ -1,61 +0,0 @@
package eur.one00kb.minipogodynka.weather;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.Locale;
public class WeatherAPIFetcher {
private static final String OM_FORMAT = "https://api.open-meteo" +
".com/v1/forecast?latitude=%f&longitude=%f&hourly=temperature_2m," +
"relative_humidity_2m,precipitation,pressure_msl,cloud_cover,wind_speed_10m," +
"wind_direction_10m,uv_index&wind_speed_unit=ms&forecast_days=7";
private static final String MET_FORMAT = "https://api.met.no/weatherapi/locationforecast/2" +
".0/complete?lat=%f&lon=%f";
// API key taken off the Internet, some GitHub example.
private static final String MF_FORMAT = "https://webservice.meteofrance" +
".com/v2/forecast?lat=%f&lon=%f" +
"&hourly=true&lang=en&token=__Wj7dVSTjV9YGu1guveLyDq0g7S7TfTjaHBTPTpO0kj8__";
public static String fetch(double lat, double lon, String provider) {
try {
URL url;
switch (provider) {
case "met":
url = new URL(String.format(Locale.US, MET_FORMAT, lat, lon));
break;
case "mf":
url = new URL(String.format(Locale.US, MF_FORMAT, lat, lon));
break;
case "om":
default:
url = new URL(String.format(Locale.US, OM_FORMAT, lat, lon));
break;
}
URLConnection conn = url.openConnection();
conn.setRequestProperty("User-Agent", "MiniPogodynka");
return isToString(conn.getInputStream());
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static String isToString(InputStream is) throws IOException {
StringBuilder builder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = reader.readLine()) != null) {
builder.append(line).append("\n");
}
reader.close();
return builder.toString();
}
}
@@ -1,132 +0,0 @@
package eur.one00kb.minipogodynka.weather;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eur.one00kb.minipogodynka.Utilities;
public class WeatherAPIParser {
private static final String TAG = "WeatherAPIParser";
private static final DateFormat OM_DATEFORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.US);
private static final DateFormat MET_DATEFORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
public static Map<Date, List<WeatherData>> parse(String json, String provider) {
switch (provider) {
case "met":
case "mf":
return null;
case "om":
default:
return parseOpenMeteo(json);
}
}
public static Map<Date, List<WeatherData>> parseOpenMeteo(String json) {
Map<Date, List<WeatherData>> weatherDays = new LinkedHashMap<>();
try {
JSONObject root = new JSONObject(json);
JSONObject hourly = root.getJSONObject("hourly");
JSONArray times = hourly.getJSONArray("time");
JSONArray temperatures = hourly.getJSONArray("temperature_2m");
JSONArray humidities = hourly.getJSONArray("relative_humidity_2m");
JSONArray precipitations = hourly.getJSONArray("precipitation");
JSONArray pressures = hourly.getJSONArray("pressure_msl");
JSONArray cloudCovers = hourly.getJSONArray("cloud_cover");
JSONArray windSpeeds = hourly.getJSONArray("wind_speed_10m");
JSONArray windDirections = hourly.getJSONArray("wind_direction_10m");
JSONArray uvIndexes = hourly.getJSONArray("uv_index");
Date currentDay = Utilities.toDayOnly(OM_DATEFORMAT.parse(times.getString(0)));
List<WeatherData> dayData = new ArrayList<>();
for (int i = 0; i < times.length(); ++i) {
Date day = Utilities.toDayOnly(OM_DATEFORMAT.parse(times.getString(i)));
if (day.getDay() != currentDay.getDay()) {
weatherDays.put(currentDay, dayData);
dayData = new ArrayList<>();
}
WeatherData hourData = new WeatherData();
hourData.date = OM_DATEFORMAT.parse(times.getString(i));
hourData.temperature = temperatures.getDouble(i);
hourData.humidity = humidities.getDouble(i);
hourData.precipitation = precipitations.getDouble(i);
hourData.pressure = pressures.getDouble(i);
hourData.cloudCover = cloudCovers.getDouble(i);
hourData.windSpeed = windSpeeds.getDouble(i);
hourData.windDirection = windDirections.getDouble(i);
hourData.uvIndex = uvIndexes.getDouble(i);
dayData.add(hourData);
currentDay = day;
}
weatherDays.put(currentDay, dayData);
} catch (JSONException | ParseException e) {
Log.e(TAG, "Error when parsing weather data", e);
}
Log.d(TAG, weatherDays.toString());
return weatherDays;
}
public static void parseMetNo(String json) {
// HashMap<Date, WeatherData> hours = new HashMap<>();
//
// try {
// JSONObject root = new JSONObject(json);
// JSONArray timeseries = root.getJSONObject("properties")
// .getJSONArray("timeseries");
//
// for (int i = 0; i < timeseries.length(); ++i) {
// JSONObject obj = timeseries.getJSONObject(i);
// JSONObject instant = obj.getJSONObject("data")
// .getJSONObject("instant").getJSONObject("details");
// JSONObject next1hr = obj.getJSONObject("data")
// .getJSONObject("next_1_hours").getJSONObject("details");
// WeatherData data = new WeatherData();
//
// data.date = MET_DATEFORMAT.parse(obj.getString("time"));
// data.temperature = instant.getDouble("air_temperature");
// data.humidity = instant.getDouble("relative_humidity");
// // I think this should be correct. If the time is 13, then it's the data for the
// // next 60 minutes, isn't it?
// data.precipitation = next1hr.getDouble("precipitation_amount");
// data.pressure = instant.getDouble("air_pressure_at_sea_level");
// data.cloudCover = instant.getDouble("cloud_area_fraction");
// data.windSpeed = instant.getDouble("wind_speed");
// data.windDirection = instant.getDouble("wind_from_direction");
// data.uvIndex = instant.getDouble("ultraviolet_index_clear_sky");
//
// hours.put(MET_DATEFORMAT.parse(obj.getString("time")), data);
// }
// } catch (JSONException | ParseException e) {
// e.printStackTrace();
// }
//
// return hours;
}
public static void parseMeteoFrance() {
}
}
@@ -8,7 +8,7 @@ public class WeatherData {
// simplify code.
public Date date;
public double temperature;
public double precipitation;
public double precipitationMm;
public double humidity;
public double pressure;
public double cloudCover;
@@ -16,10 +16,33 @@ public class WeatherData {
public double windDirection;
public double uvIndex;
public int getWeatherCode() {
// Manually calculating the weather code based on the variables. Probably not
// entirely correct, but good enough anyway.
//https://www.baranidesign.com/faq-articles/2020/1/19/rain-rate-intensity-classification
if (precipitationMm > 7.5) {
return 8;
} else if (precipitationMm > 2.5) {
return 60;
} else if (precipitationMm > 0) {
return 33;
}
//https://www.pagasa.dost.gov.ph/information/weather-terminologies
if (cloudCover > 70) {
return 3; //Cloudy
} else if (cloudCover > 30) {
return 57; //Partly cloudy
} else {
return 0; //Clearsky
}
}
public MemberInfo[] getMemberInfo() {
return new MemberInfo[]{
new MemberInfo("Temperature", temperature, "°C"),
new MemberInfo("Precipitation", precipitation, "%"),
new MemberInfo("Precipitation", precipitationMm, "mm"),
new MemberInfo("Humidity", humidity, "%"),
new MemberInfo("Pressure", pressure, "hPa"),
new MemberInfo("Cloud cover", cloudCover, "%"),
@@ -0,0 +1,65 @@
package eur.one00kb.minipogodynka.weather;
import android.content.Context;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eur.one00kb.minipogodynka.R;
// TODO: For all data providers: check if the data parsed is correct and inline with the JSON.
// The exception handling part stinks.
public abstract class WeatherDataProvider {
private final String mUserAgent;
/**
* Must contain two floating point format specifiers for latitude and longitude, in order
*/
private String mAPILinkFormat;
public WeatherDataProvider(Context context) {
mUserAgent = String.format("%s", context.getString(R.string.app_name));
}
public void setAPILinkFormat(String format) {
mAPILinkFormat = format;
}
public Map<Date, List<WeatherData>> provide(double latitude, double longitude)
throws IOException {
String data = fetchData(latitude, longitude);
return parseData(data);
}
/**
* @param data The JSON or otherwise data to parse.
* @return A sorted hashmap with keys being Dates with just the day, month, and year, and the
* values being lists of hourly weather data.
*/
public abstract Map<Date, List<WeatherData>> parseData(String data);
public String fetchData(double latitude, double longitude) throws IOException {
URLConnection conn =
new URL(String.format(Locale.US, mAPILinkFormat, latitude, longitude))
.openConnection();
conn.setRequestProperty("User-Agent", mUserAgent);
StringBuilder builder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
builder.append(line).append("\n");
}
reader.close();
return builder.toString();
}
}
@@ -0,0 +1,22 @@
package eur.one00kb.minipogodynka.weather;
import java.util.HashMap;
import java.util.Map;
import eur.one00kb.minipogodynka.R;
public class WeatherUtils {
private static final Map<Integer, Integer> mCodeIcons = new HashMap<Integer, Integer>() {{
put(0, R.drawable.ic_weather_clearsky); //clearsky_day
put(3, R.drawable.ic_weather_cloudy);
put(4, R.drawable.ic_weather_fair); //fair_day
put(8, R.drawable.ic_weather_rain); //TBA, heavy_rain
put(33, R.drawable.ic_weather_rain); //TBA, lightrain
put(57, R.drawable.ic_weather_fair); //TBA, partlycloudy_day
put(60, R.drawable.ic_weather_rain);
}};
public static int getDrawableforCode(int code) {
return mCodeIcons.get(code);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M13.95,13H9V8.05l5.61,-5.61C13.78,2.16 12.9,2 12,2c-4.2,0 -8,3.22 -8,8.2c0,3.32 2.67,7.25 8,11.8c5.33,-4.55 8,-8.48 8,-11.8c0,-1.01 -0.16,-1.94 -0.45,-2.8L13.95,13z"/>
<path android:fillColor="@android:color/white" android:pathData="M11,11l2.12,0l6.16,-6.16l-2.12,-2.12l-6.16,6.16z"/>
<path android:fillColor="@android:color/white" android:pathData="M20.71,2L20,1.29C19.8,1.1 19.55,1 19.29,1c-0.13,0 -0.48,0.07 -0.71,0.29l-0.72,0.72l2.12,2.12l0.72,-0.72C21.1,3.02 21.1,2.39 20.71,2z"/>
</vector>
+5
View File
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<stroke android:color="#FFDF00"
android:dashGap="6dp"
android:dashWidth="4dp"
android:width="8dp" />
</shape>
</item>
<item android:top="8dp"
android:bottom="8dp"
android:left="8dp"
android:right="8dp">
<shape android:shape="oval">
<solid android:color="#FFCF00" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:top="10dp"
android:right="16dp"
android:bottom="6dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="10dp"
android:right="0dp"
android:left="16dp"
android:bottom="6dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="4dp"
android:right="8dp"
android:left="8dp"
android:bottom="12dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="26dp"
android:left="8dp"
android:right="8dp"
android:bottom="6dp">
<shape android:shape="rectangle">
<solid android:color="#DDDDDD" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<stroke android:color="#FFDF00"
android:dashGap="6dp"
android:dashWidth="4dp"
android:width="8dp" />
</shape>
</item>
<item android:top="8dp"
android:bottom="8dp"
android:left="8dp"
android:right="8dp">
<shape android:shape="oval">
<solid android:color="#FFCF00" />
</shape>
</item>
<item android:top="18dp"
android:left="24dp"
android:bottom="6dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="18dp"
android:left="16dp"
android:right="8dp"
android:bottom="6dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="14dp"
android:left="20dp"
android:right="4dp"
android:bottom="10dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="28dp"
android:left="20dp"
android:right="4dp"
android:bottom="6dp">
<shape android:shape="rectangle">
<solid android:color="#DDDDDD" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rain-->
<item android:left="12dp"
android:right="12dp"
android:top="18dp"
android:bottom="-999dp"> <!-- Heh… -->
<shape android:shape="rectangle">
<stroke android:color="#0000FF"
android:width="4dp"
android:dashWidth="2dp"
android:dashGap="1dp" />
</shape>
</item>
<!-- Clouds-->
<item android:top="8dp"
android:right="16dp"
android:bottom="8dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="8dp"
android:right="0dp"
android:left="16dp"
android:bottom="8dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="2dp"
android:right="8dp"
android:left="8dp"
android:bottom="14dp">
<shape android:shape="oval">
<solid android:color="#DDDDDD" />
</shape>
</item>
<item android:top="24dp"
android:left="8dp"
android:right="8dp"
android:bottom="8dp">
<shape android:shape="rectangle">
<solid android:color="#DDDDDD" />
</shape>
</item>
</layer-list>
+2 -3
View File
@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_height="@dimen/hourly_group_height"
android:gravity="right|center_vertical"
android:padding="8dp"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="24sp">
</TextView>
+16 -1
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="@dimen/hourly_item_height"
android:orientation="horizontal"
android:padding="8dp">
@@ -14,6 +16,19 @@
android:text="@string/placeholder"
android:textSize="16sp" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/hour_icon"
android:layout_width="@dimen/hourly_icon_size"
android:layout_height="@dimen/hourly_icon_size"
android:layout_gravity="center"
android:src="@drawable/ic_weather_clearsky" />
</FrameLayout>
<TextView
android:id="@+id/hour_temp"
android:layout_width="0dp"
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<eur.one00kb.minipogodynka.views.NowView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/now_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:src="@drawable/bg"
android:tint="#DDDDDD" />
<ImageView
android:id="@+id/imageView2"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_centerInParent="true"
android:src="@drawable/ic_weather_clearsky" />
</eur.one00kb.minipogodynka.views.NowView>
+6 -5
View File
@@ -2,15 +2,16 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_update"
android:showAsAction="ifRoom"
android:title="@string/menu_item_update" />
android:id="@+id/action_refresh"
android:icon="@drawable/ic_refresh"
android:showAsAction="always"
android:title="@string/menu_item_refresh" />
<item
android:id="@+id/action_location"
android:showAsAction="ifRoom"
android:icon="@drawable/ic_location"
android:showAsAction="always"
android:title="@string/menu_item_location" />
<item
android:id="@+id/action_settings"
android:showAsAction="ifRoom"
android:title="@string/menu_item_settings" />
</menu>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="hourly_item_height">48dp</dimen>
<dimen name="hourly_group_height">64dp</dimen>
<dimen name="hourly_icon_size">36dp</dimen>
</resources>
+4 -1
View File
@@ -4,12 +4,13 @@
<string name="settings_name">Settings</string>
<string name="placeholder">Placeholder</string>
<string name="no_location">No location selected</string>
<string name="dialog_search_title">Search for location</string>
<string name="dialog_search_field">City, street, area…</string>
<string name="dialog_search_error">Geocoder unavailable</string>
<string name="menu_item_update">Update</string>
<string name="menu_item_refresh">Refresh</string>
<string name="menu_item_location">Location</string>
<string name="menu_item_settings">Settings</string>
@@ -17,6 +18,8 @@
<string name="preferences_title_provider">Weather provider</string>
<string name="preferences_default_provider">om</string>
<string name="preferences_key_location">location</string>
<string name="preferences_summary_copyright_info">
Copyleft 🄯 Emil Kosz, 2025. License: GPLv3
</string>