Icons, minor improvements, refactoring of weather providers
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
|
||||
Reference in New Issue
Block a user