diff --git a/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java b/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java index 301ce40..c21f9fc 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java @@ -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 mWeatherDataProviders; private Map> 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(); } diff --git a/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java b/app/src/main/java/eur/one00kb/minipogodynka/Utils.java similarity index 96% rename from app/src/main/java/eur/one00kb/minipogodynka/Utilities.java rename to app/src/main/java/eur/one00kb/minipogodynka/Utils.java index 1b2e1e3..c483c86 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/Utils.java @@ -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); diff --git a/app/src/main/java/eur/one00kb/minipogodynka/adapters/HourlyAdapter.java b/app/src/main/java/eur/one00kb/minipogodynka/adapters/HourlyAdapter.java index be54cee..ad67619 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/adapters/HourlyAdapter.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/adapters/HourlyAdapter.java @@ -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 mKeys; private final Map> mData; @@ -28,10 +30,10 @@ public class HourlyAdapter extends BaseExpandableListAdapter { mData = new LinkedHashMap<>(); } - public void setKeysAndData(List keys, Map> data) { + public void setData(Map> 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, % 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(""); diff --git a/app/src/main/java/eur/one00kb/minipogodynka/views/NowView.java b/app/src/main/java/eur/one00kb/minipogodynka/views/NowView.java new file mode 100644 index 0000000..0386ea6 --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/views/NowView.java @@ -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(); + } +} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/MetNoProvider.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/MetNoProvider.java new file mode 100644 index 0000000..23b90c4 --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/weather/MetNoProvider.java @@ -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> parseData(String data) { + Map> 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 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; + } +} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/MeteoFranceProvider.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/MeteoFranceProvider.java new file mode 100644 index 0000000..db60599 --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/weather/MeteoFranceProvider.java @@ -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> parseData(String data) { + Map> 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 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; + } +} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/OpenMeteoProvider.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/OpenMeteoProvider.java new file mode 100644 index 0000000..6c3f01e --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/weather/OpenMeteoProvider.java @@ -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> parseData(String data) { + Map> 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 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; + } +} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIFetcher.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIFetcher.java deleted file mode 100644 index 0fe407d..0000000 --- a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIFetcher.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIParser.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIParser.java deleted file mode 100644 index ee4c966..0000000 --- a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIParser.java +++ /dev/null @@ -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> parse(String json, String provider) { - switch (provider) { - case "met": - case "mf": - return null; - case "om": - default: - return parseOpenMeteo(json); - } - } - - public static Map> parseOpenMeteo(String json) { - Map> 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 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 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() { - - } -} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherData.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherData.java index 22392f7..4f1cf1c 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherData.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherData.java @@ -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, "%"), diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherDataProvider.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherDataProvider.java new file mode 100644 index 0000000..ad4f635 --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherDataProvider.java @@ -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> 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> 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(); + } +} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherUtils.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherUtils.java new file mode 100644 index 0000000..e3bd152 --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherUtils.java @@ -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 mCodeIcons = new HashMap() {{ + 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); + } +} diff --git a/app/src/main/res/drawable/bg.webp b/app/src/main/res/drawable/bg.webp new file mode 100644 index 0000000..f53b14b Binary files /dev/null and b/app/src/main/res/drawable/bg.webp differ diff --git a/app/src/main/res/drawable/ic_location.xml b/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 0000000..804d536 --- /dev/null +++ b/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..0bb6637 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_weather_clearsky.xml b/app/src/main/res/drawable/ic_weather_clearsky.xml new file mode 100644 index 0000000..9ac1227 --- /dev/null +++ b/app/src/main/res/drawable/ic_weather_clearsky.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_weather_cloudy.xml b/app/src/main/res/drawable/ic_weather_cloudy.xml new file mode 100644 index 0000000..b2783b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_weather_cloudy.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_weather_fair.xml b/app/src/main/res/drawable/ic_weather_fair.xml new file mode 100644 index 0000000..687f916 --- /dev/null +++ b/app/src/main/res/drawable/ic_weather_fair.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_weather_rain.xml b/app/src/main/res/drawable/ic_weather_rain.xml new file mode 100644 index 0000000..e2bbb43 --- /dev/null +++ b/app/src/main/res/drawable/ic_weather_rain.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_group.xml b/app/src/main/res/layout/item_group.xml index 8eed1a9..eece4c9 100644 --- a/app/src/main/res/layout/item_group.xml +++ b/app/src/main/res/layout/item_group.xml @@ -1,10 +1,9 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/item_hour.xml b/app/src/main/res/layout/item_hour.xml index 61794f0..291d938 100644 --- a/app/src/main/res/layout/item_hour.xml +++ b/app/src/main/res/layout/item_hour.xml @@ -1,7 +1,9 @@ @@ -14,6 +16,19 @@ android:text="@string/placeholder" android:textSize="16sp" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 67f422f..f1ac7b8 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -2,15 +2,16 @@ + android:id="@+id/action_refresh" + android:icon="@drawable/ic_refresh" + android:showAsAction="always" + android:title="@string/menu_item_refresh" /> \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml new file mode 100644 index 0000000..f5846be --- /dev/null +++ b/app/src/main/res/values/dimen.xml @@ -0,0 +1,6 @@ + + + 48dp + 64dp + 36dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a81a2e..7098de9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,12 +4,13 @@ Settings Placeholder + No location selected Search for location City, street, area… Geocoder unavailable - Update + Refresh Location Settings @@ -17,6 +18,8 @@ Weather provider om + location + Copyleft 🄯 Emil Kosz, 2025. License: GPLv3