Hourly data grouped by day, setting location updates data

This commit is contained in:
2025-01-29 22:10:56 +01:00
parent 1cf19b3934
commit 6f0eb478cc
11 changed files with 238 additions and 134 deletions
@@ -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<Date, WeatherData> mWeatherData;
private Map<Date, List<WeatherData>> 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, %<tA", data.date))
.setAdapter(new DetailsAdapter(this, data.getMemberInfo()), null)
.show();
mHourlyView = new ExpandableListView(this);
mHourlyView.setAdapter(mHourlyAdapter);
mHourlyView.setOnChildClickListener((parent, view, groupPosition, childPosition, id) -> {
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, %<tA", data.date));
mDetailsDialog.show();
return false;
});
getTabHost().addTab(getTabHost().newTabSpec("hourly")
@@ -88,9 +100,9 @@ public class MainActivity extends TabActivity {
int id = item.getItemId();
if (id == R.id.action_update) {
updateApiData();
fetchApiData(this::updateInfo);
} else if (id == R.id.action_location) {
mLocationDialog.show();
mLocationSearchDialog.show();
} else if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
}
@@ -101,25 +113,16 @@ public class MainActivity extends TabActivity {
public void setAddress(Address address) {
mCurrentAddress = address;
setTitle(mCurrentAddress.getAddressLine(0));
}
public void dismissLocationDialog() {
mLocationDialog.dismiss();
mLocationSearchDialog.dismiss();
fetchApiData(this::updateInfo);
}
private void updateInfo() {
Date forApiDate = Utilities.getDateOnly();
ArrayList<WeatherData> 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();
}
}
@@ -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();
}
@@ -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;
@@ -11,8 +11,8 @@ import eur.one00kb.minipogodynka.R;
import eur.one00kb.minipogodynka.weather.WeatherData;
public class DetailsAdapter extends ArrayAdapter<WeatherData.MemberInfo> {
public DetailsAdapter(Context context, WeatherData.MemberInfo[] objects) {
super(context, 0, objects);
public DetailsAdapter(Context context) {
super(context, 0);
}
@Override
@@ -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<WeatherData> {
public HourlyAdapter(Context context, List<WeatherData> objects) {
super(context, 0, objects);
public class HourlyAdapter extends BaseExpandableListAdapter {
private Context mContext;
private final List<Date> mKeys;
private final Map<Date, List<WeatherData>> mData;
public HourlyAdapter(Context context) {
mContext = context;
mKeys = new ArrayList<>();
mData = new LinkedHashMap<>();
}
public void setKeysAndData(List<Date> keys, Map<Date, List<WeatherData>> 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, %<td",
(Date) getGroup(groupPosition)));
return convertView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_hour, null);
}
WeatherData child = (WeatherData) getChild(groupPosition, childPosition);
((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));
((TextView) convertView.findViewById(R.id.hour_precp))
.setText(String.format("%.0f %%", child.precipitation));
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
}
@@ -1,16 +1,12 @@
package eur.one00kb.minipogodynka;
package eur.one00kb.minipogodynka.views;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.location.Address;
import android.location.Geocoder;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
@@ -24,10 +20,14 @@ import java.util.Collections;
import java.util.List;
import java.util.Locale;
import eur.one00kb.minipogodynka.MainActivity;
import eur.one00kb.minipogodynka.R;
import eur.one00kb.minipogodynka.adapters.AddressAdapter;
// TODO: This doesn't need to have a parent. However, for some reason it screams with a bunch of
// exceptions when you extend AutoCompleteTextView.
// TODO: Make this tabbed, and add coordinates. The dialog neutral button will then be used for
// geolocating.
public class LocationSearchView extends LinearLayout
implements TextWatcher, AdapterView.OnItemClickListener {
@@ -94,8 +94,6 @@ public class LocationSearchView extends LinearLayout
@Override
public void onItemClick(AdapterView<?> 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("");
}
@@ -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<Date, WeatherData> parse(String json, String provider) {
public static Map<Date, List<WeatherData>> 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<Date, WeatherData> parseOpenMeteo(String json) {
HashMap<Date, WeatherData> hours = new HashMap<>();
public static Map<Date, List<WeatherData>> parseOpenMeteo(String json) {
Map<Date, List<WeatherData>> 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<WeatherData> 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<Date, WeatherData> 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 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() {
@@ -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;
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<eur.one00kb.minipogodynka.LocationSearchView xmlns:android="http://schemas.android.com/apk/res/android"
<eur.one00kb.minipogodynka.views.LocationSearchView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@@ -9,9 +9,9 @@
<AutoCompleteTextView
android:id="@+id/dl_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:completionThreshold="2"
android:hint="@string/dialog_search_field"
android:dropDownHeight="170dp" />
android:dropDownHeight="170dp"
android:hint="@string/dialog_search_field" />
</eur.one00kb.minipogodynka.LocationSearchView>
</eur.one00kb.minipogodynka.views.LocationSearchView>
+10
View File
@@ -0,0 +1,10 @@
<?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:padding="8dp"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="24sp">
</TextView>
+2 -4
View File
@@ -2,18 +2,16 @@
<resources>
<string name="app_name">MiniPogodynka</string>
<string name="settings_name">Settings</string>
<string name="location_name">Location</string>
<string name="placeholder">Placeholder</string>
<string name="dialog_search_title">Set location</string>
<string name="dialog_search_field">Search for location</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_location">Location</string>
<string name="menu_item_settings">Settings</string>
<string name="menu_item_search">Search</string>
<string name="preferences_key_provider">weather_provider</string>
<string name="preferences_title_provider">Weather provider</string>