From 6f0eb478cca30c16913ac7220d7de6b12c166402 Mon Sep 17 00:00:00 2001 From: Emil Kosz Date: Wed, 29 Jan 2025 22:10:56 +0100 Subject: [PATCH] Hourly data grouped by day, setting location updates data --- .../one00kb/minipogodynka/MainActivity.java | 81 +++++------ .../eur/one00kb/minipogodynka/Utilities.java | 11 +- .../adapters/AddressAdapter.java | 2 - .../adapters/DetailsAdapter.java | 4 +- .../minipogodynka/adapters/HourlyAdapter.java | 103 +++++++++++--- .../{ => views}/LocationSearchView.java | 12 +- .../weather/WeatherAPIParser.java | 127 ++++++++++-------- .../minipogodynka/weather/WeatherData.java | 6 +- ...{dialog_location.xml => dialog_search.xml} | 10 +- app/src/main/res/layout/item_group.xml | 10 ++ app/src/main/res/values/strings.xml | 6 +- 11 files changed, 238 insertions(+), 134 deletions(-) rename app/src/main/java/eur/one00kb/minipogodynka/{ => views}/LocationSearchView.java (94%) rename app/src/main/res/layout/{dialog_location.xml => dialog_search.xml} (54%) create mode 100644 app/src/main/res/layout/item_group.xml diff --git a/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java b/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java index 0bd0010..301ce40 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java @@ -4,15 +4,15 @@ import android.app.AlertDialog; import android.app.TabActivity; import android.content.Intent; import android.location.Address; -import android.location.Location; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; +import android.widget.ExpandableListView; import android.widget.ListView; import java.util.ArrayList; import java.util.Date; -import java.util.Locale; +import java.util.List; import java.util.Map; import eur.one00kb.minipogodynka.adapters.DetailsAdapter; @@ -21,7 +21,6 @@ import eur.one00kb.minipogodynka.weather.WeatherAPIFetcher; import eur.one00kb.minipogodynka.weather.WeatherAPIParser; import eur.one00kb.minipogodynka.weather.WeatherData; -// TODO: Searching functionality // TODO: Location history // TODO: Replace Date with Calendar, apparently it's better // TODO: Action icons @@ -34,40 +33,53 @@ public class MainActivity extends TabActivity { private PreferenceHolder mPreferenceHolder; private Address mCurrentAddress; - private Map mWeatherData; + private Map> mWeatherData; - private LocationSearchView mLocationSearchView; - private ListView mHourlyView; + private HourlyAdapter mHourlyAdapter; + private DetailsAdapter mDetailsAdapter; + private ExpandableListView mHourlyView; - private AlertDialog mLocationDialog; - private AlertDialog.Builder mDetailsDialogBuilder; + private AlertDialog mLocationSearchDialog; + private AlertDialog mDetailsDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPreferenceHolder = new PreferenceHolder(this); + mHourlyAdapter = new HourlyAdapter(this); + mDetailsAdapter = new DetailsAdapter(this); - mLocationSearchView = (LocationSearchView) - getLayoutInflater().inflate(R.layout.dialog_location, null); - - mLocationDialog = new AlertDialog.Builder(this) + mLocationSearchDialog = new AlertDialog.Builder(this) .setCancelable(false) .setNegativeButton(android.R.string.cancel, null) .setTitle(R.string.dialog_search_title) - .setView(mLocationSearchView).create(); + .setView(getLayoutInflater().inflate(R.layout.dialog_search, null)) + .create(); - mDetailsDialogBuilder = new AlertDialog.Builder(this) + mDetailsDialog = new AlertDialog.Builder(this) .setCancelable(false) - .setPositiveButton(android.R.string.ok, null); + .setPositiveButton(android.R.string.ok, null) + .setAdapter(mDetailsAdapter, null) + .create(); - mHourlyView = new ListView(this); - mHourlyView.setOnItemClickListener((parent, view, position, id) -> { - WeatherData data = (WeatherData) mHourlyView.getItemAtPosition(position); - mDetailsDialogBuilder - .setTitle(String.format("%tR, % { + WeatherData data = (WeatherData) mHourlyAdapter.getChild(groupPosition, childPosition); + + mDetailsAdapter.clear(); + + for (WeatherData.MemberInfo info : data.getMemberInfo()) { + mDetailsAdapter.add(info); + } + + mDetailsAdapter.notifyDataSetChanged(); + mDetailsDialog.setTitle(String.format("%tR, % forAdapter = new ArrayList<>(); - - for (int i = forApiDate.getHours(); i < 24; ++i) { - forApiDate.setHours(i); - forAdapter.add(mWeatherData.get(forApiDate)); - } - - mHourlyView.setAdapter(new HourlyAdapter(this, forAdapter)); + mHourlyAdapter.setKeysAndData(new ArrayList<>(mWeatherData.keySet()), mWeatherData); + mHourlyAdapter.notifyDataSetChanged(); } - private void updateApiData() { + private void fetchApiData(Runnable afterCallback) { if (mCurrentAddress == null) { return; } @@ -130,7 +133,7 @@ public class MainActivity extends TabActivity { String apiData = WeatherAPIFetcher.fetch(mCurrentAddress.getLatitude(), mCurrentAddress.getLongitude(), mPreferenceHolder.getProvider()); mWeatherData = WeatherAPIParser.parse(apiData, mPreferenceHolder.getProvider()); - runOnUiThread(this::updateInfo); + runOnUiThread(afterCallback); }).start(); } } diff --git a/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java b/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java index 04e25f3..1b2e1e3 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java @@ -6,7 +6,7 @@ import android.widget.Toast; import java.util.Date; public class Utilities { - public static Date getDateOnly() { + public static Date getDayOnly() { Date date = new Date(); date.setHours(0); date.setMinutes(0); @@ -15,6 +15,15 @@ public class Utilities { return date; } + public static Date toDayOnly(Date date) { + Date clone = (Date) date.clone(); + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setTime(date.getTime() - (date.getTime() % 1000)); + return clone; + } + public static void quickToast(Context context, String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } diff --git a/app/src/main/java/eur/one00kb/minipogodynka/adapters/AddressAdapter.java b/app/src/main/java/eur/one00kb/minipogodynka/adapters/AddressAdapter.java index f0daad1..2a570d2 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/adapters/AddressAdapter.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/adapters/AddressAdapter.java @@ -6,8 +6,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.Filter; -import android.widget.Filterable; import android.widget.TextView; import java.util.ArrayList; diff --git a/app/src/main/java/eur/one00kb/minipogodynka/adapters/DetailsAdapter.java b/app/src/main/java/eur/one00kb/minipogodynka/adapters/DetailsAdapter.java index 850a3d9..eae4c74 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/adapters/DetailsAdapter.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/adapters/DetailsAdapter.java @@ -11,8 +11,8 @@ import eur.one00kb.minipogodynka.R; import eur.one00kb.minipogodynka.weather.WeatherData; public class DetailsAdapter extends ArrayAdapter { - public DetailsAdapter(Context context, WeatherData.MemberInfo[] objects) { - super(context, 0, objects); + public DetailsAdapter(Context context) { + super(context, 0); } @Override 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 475c7de..be54cee 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/adapters/HourlyAdapter.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/adapters/HourlyAdapter.java @@ -1,41 +1,108 @@ 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.ArrayAdapter; +import android.widget.BaseExpandableListAdapter; import android.widget.TextView; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import eur.one00kb.minipogodynka.R; import eur.one00kb.minipogodynka.weather.WeatherData; -public class HourlyAdapter extends ArrayAdapter { - public HourlyAdapter(Context context, List objects) { - super(context, 0, objects); +public class HourlyAdapter extends BaseExpandableListAdapter { + private Context mContext; + private final List mKeys; + private final Map> mData; + + public HourlyAdapter(Context context) { + mContext = context; + mKeys = new ArrayList<>(); + mData = new LinkedHashMap<>(); + } + + public void setKeysAndData(List keys, Map> data) { + mKeys.clear(); + mData.clear(); + mKeys.addAll(keys); + mData.putAll(data); } @Override - public View getView(int position, View convertView, ViewGroup parent) { + public int getGroupCount() { + return mKeys.size(); + } + + @Override + public int getChildrenCount(int groupPosition) { + return mData.get(mKeys.get(groupPosition)).size(); + } + + @Override + public Object getGroup(int groupPosition) { + return mKeys.get(groupPosition); + } + + @Override + public Object getChild(int groupPosition, int childPosition) { + return mData.get(mKeys.get(groupPosition)).get(childPosition); + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { if (convertView == null) { - convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_hour, parent, - false); + convertView = LayoutInflater.from(mContext) + .inflate(R.layout.item_group, null); } - WeatherData data = getItem(position); - TextView time = convertView.findViewById(R.id.hour_time); - TextView temperature = convertView.findViewById(R.id.hour_temp); - TextView precipitation = convertView.findViewById(R.id.hour_precp); - - if (data != null) { - time.setText(String.format("%tR", data.date)); - // No one cares about the decimals. - temperature.setText(String.format("%d °C", Math.round(data.temperature))); - precipitation.setText(String.format("%d %%", Math.round(data.precipitation))); - } + ((TextView) convertView).setText(String.format("%tA, % parent, View view, int position, long id) { ((MainActivity) getContext()).setAddress(mAddressAdapter.getItem(position)); - ((MainActivity) getContext()).dismissLocationDialog(); - AutoCompleteTextView mSearchField = findViewById(R.id.dl_search); mSearchField.setText(""); } diff --git a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIParser.java b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIParser.java index b5111b7..ee4c966 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIParser.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIParser.java @@ -1,5 +1,7 @@ package eur.one00kb.minipogodynka.weather; +import android.util.Log; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -7,21 +9,26 @@ 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) { + public static Map> parse(String json, String provider) { switch (provider) { case "met": - return parseMetNo(json); case "mf": return null; case "om": @@ -30,8 +37,8 @@ public class WeatherAPIParser { } } - public static Map parseOpenMeteo(String json) { - HashMap hours = new HashMap<>(); + public static Map> parseOpenMeteo(String json) { + Map> weatherDays = new LinkedHashMap<>(); try { JSONObject root = new JSONObject(json); @@ -47,62 +54,76 @@ public class WeatherAPIParser { JSONArray windDirections = hourly.getJSONArray("wind_direction_10m"); JSONArray uvIndexes = hourly.getJSONArray("uv_index"); - for (int i = 0; i < times.length(); ++i) { - WeatherData data = new WeatherData(); - data.date = OM_DATEFORMAT.parse(times.getString(i)); - data.temperature = temperatures.getDouble(i); - data.humidity = humidities.getDouble(i); - data.precipitation = precipitations.getDouble(i); - data.pressure = pressures.getDouble(i); - data.cloudCover = cloudCovers.getDouble(i); - data.windSpeed = windSpeeds.getDouble(i); - data.windDirection = windDirections.getDouble(i); - data.uvIndex = uvIndexes.getDouble(i); + Date currentDay = Utilities.toDayOnly(OM_DATEFORMAT.parse(times.getString(0))); + List dayData = new ArrayList<>(); - hours.put(OM_DATEFORMAT.parse(times.getString(i)), data); + 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) { - e.printStackTrace(); + Log.e(TAG, "Error when parsing weather data", e); } - return hours; + Log.d(TAG, weatherDays.toString()); + return weatherDays; } - public static Map 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 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 93f73d1..22392f7 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherData.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherData.java @@ -30,9 +30,9 @@ public class WeatherData { } public static class MemberInfo { - public String label; - public Double value; - public String unit; + public final String label; + public final Double value; + public final String unit; public MemberInfo(String labela, Double valuea, String unita) { label = labela; diff --git a/app/src/main/res/layout/dialog_location.xml b/app/src/main/res/layout/dialog_search.xml similarity index 54% rename from app/src/main/res/layout/dialog_location.xml rename to app/src/main/res/layout/dialog_search.xml index d052959..ceb2188 100644 --- a/app/src/main/res/layout/dialog_location.xml +++ b/app/src/main/res/layout/dialog_search.xml @@ -1,5 +1,5 @@ - + android:dropDownHeight="170dp" + android:hint="@string/dialog_search_field" /> - \ No newline at end of file + \ 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 new file mode 100644 index 0000000..8eed1a9 --- /dev/null +++ b/app/src/main/res/layout/item_group.xml @@ -0,0 +1,10 @@ + + + + \ 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 c40c7ce..2a81a2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,18 +2,16 @@ MiniPogodynka Settings - Location Placeholder - Set location - Search for location + Search for location + City, street, area… Geocoder unavailable Update Location Settings - Search weather_provider Weather provider