/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.apps.dashclock.weather;
import android.location.Location;
import android.text.TextUtils;
import android.util.Pair;
import com.google.android.apps.dashclock.LogUtils;
import com.google.android.apps.dashclock.Utils;
import net.nurik.roman.dashclock.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static com.google.android.apps.dashclock.LogUtils.LOGD;
import static com.google.android.apps.dashclock.LogUtils.LOGE;
import static com.google.android.apps.dashclock.LogUtils.LOGW;
/**
* Client code for the Yahoo! Weather RSS feeds and GeoPlanet API.
*/
class YahooWeatherApiClient {
private static final String TAG = LogUtils.makeLogTag(YahooWeatherApiClient.class);
private static String sWeatherUnits;
private static XmlPullParserFactory sXmlPullParserFactory;
private static final int MAX_SEARCH_RESULTS = 10;
static {
try {
sXmlPullParserFactory = XmlPullParserFactory.newInstance();
sXmlPullParserFactory.setNamespaceAware(true);
} catch (XmlPullParserException e) {
LOGE(TAG, "Could not instantiate XmlPullParserFactory", e);
}
}
public static void setWeatherUnits(String weatherUnits) {
sWeatherUnits = weatherUnits;
}
public static WeatherData getWeatherForLocationInfo(LocationInfo locationInfo)
throws CantGetWeatherException {
// Loop through the woeids (they're in descending precision order) until weather data
// is found.
for (String woeid : locationInfo.woeids) {
LOGD(TAG, "Trying WOEID: " + woeid);
WeatherData data = YahooWeatherApiClient.getWeatherForWoeid(woeid, locationInfo.town);
if (data != null
&& data.conditionCode != WeatherData.INVALID_CONDITION
&& data.temperature != WeatherData.INVALID_TEMPERATURE) {
return data;
}
}
// No weather could be found :(
throw new CantGetWeatherException(true, R.string.no_weather_data);
}
public static WeatherData getWeatherForWoeid(String woeid, String town)
throws CantGetWeatherException {
HttpURLConnection connection = null;
try {
// TODO: proper http library
connection = Utils.openUrlConnection(buildWeatherQueryUrl(woeid));
XmlPullParser xpp = sXmlPullParserFactory.newPullParser();
xpp.setInput(new InputStreamReader(connection.getInputStream()));
WeatherData data = new WeatherData();
boolean hasTodayForecast = false;
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG
&& "condition".equals(xpp.getName())) {
for (int i = xpp.getAttributeCount() - 1; i >= 0; i--) {
if ("temp".equals(xpp.getAttributeName(i))) {
data.temperature = Integer.parseInt(xpp.getAttributeValue(i));
} else if ("code".equals(xpp.getAttributeName(i))) {
data.conditionCode = Integer.parseInt(xpp.getAttributeValue(i));
} else if ("text".equals(xpp.getAttributeName(i))) {
data.conditionText = xpp.getAttributeValue(i);
}
}
} else if (eventType == XmlPullParser.START_TAG
&& "forecast".equals(xpp.getName())
&& !hasTodayForecast) {
// TODO: verify this is the forecast for today (this currently assumes the
// first forecast is today's forecast)
hasTodayForecast = true;
for (int i = xpp.getAttributeCount() - 1; i >= 0; i--) {
if ("code".equals(xpp.getAttributeName(i))) {
data.todayForecastConditionCode
= Integer.parseInt(xpp.getAttributeValue(i));
} else if ("low".equals(xpp.getAttributeName(i))) {
data.low = Integer.parseInt(xpp.getAttributeValue(i));
} else if ("high".equals(xpp.getAttributeName(i))) {
data.high = Integer.parseInt(xpp.getAttributeValue(i));
} else if ("text".equals(xpp.getAttributeName(i))) {
data.forecastText = xpp.getAttributeValue(i);
}
}
} else if (eventType == XmlPullParser.START_TAG
&& "location".equals(xpp.getName())) {
String cityOrVillage = "--";
String region = null;
String country = "--";
for (int i = xpp.getAttributeCount() - 1; i >= 0; i--) {
if ("city".equals(xpp.getAttributeName(i))) {
cityOrVillage = xpp.getAttributeValue(i);
} else if ("region".equals(xpp.getAttributeName(i))) {
region = xpp.getAttributeValue(i);
} else if ("country".equals(xpp.getAttributeName(i))) {
country = xpp.getAttributeValue(i);
}
}
if (TextUtils.isEmpty(region)) {
// If no region is available, show the country. Otherwise, don't
// show country information.
region = country;
}
if (!TextUtils.isEmpty(town) && !town.equals(cityOrVillage)) {
// If a town is available and it's not equivalent to the city name,
// show it.
cityOrVillage = cityOrVillage + ", " + town;
}
data.location = cityOrVillage + ", " + region;
}
eventType = xpp.next();
}
if (TextUtils.isEmpty(data.location)) {
data.location = town;
}
return data;
} catch (IOException e) {
throw new CantGetWeatherException(true, R.string.no_weather_data,
"Error parsing weather feed XML.", e);
} catch (NumberFormatException e) {
throw new CantGetWeatherException(true, R.string.no_weather_data,
"Error parsing weather feed XML.", e);
} catch (XmlPullParserException e) {
throw new CantGetWeatherException(true, R.string.no_weather_data,
"Error parsing weather feed XML.", e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
public static LocationInfo getLocationInfo(Location location) throws CantGetWeatherException {
LocationInfo li = new LocationInfo();
// first=tagname (admin1, locality3) second=woeid
String primaryWoeid = null;
List<Pair<String, String>> alternateWoeids = new ArrayList<Pair<String, String>>();
HttpURLConnection connection = null;
try {
connection = Utils.openUrlConnection(buildPlaceSearchUrl(location));
XmlPullParser xpp = sXmlPullParserFactory.newPullParser();
xpp.setInput(new InputStreamReader(connection.getInputStream()));
boolean inWoe = false;
boolean inTown = false;
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
String tagName = xpp.getName();
if (eventType == XmlPullParser.START_TAG && "woeid".equals(tagName)) {
inWoe = true;
} else if (eventType == XmlPullParser.TEXT && inWoe) {
primaryWoeid = xpp.getText();
}
if (eventType == XmlPullParser.START_TAG &&
(tagName.startsWith("locality") || tagName.startsWith("admin"))) {
for (int i = xpp.getAttributeCount() - 1; i >= 0; i--) {
String attrName = xpp.getAttributeName(i);
if ("type".equals(attrName)
&& "Town".equals(xpp.getAttributeValue(i))) {
inTown = true;
} else if ("woeid".equals(attrName)) {
String woeid = xpp.getAttributeValue(i);
if (!TextUtils.isEmpty(woeid)) {
alternateWoeids.add(
new Pair<String, String>(tagName, woeid));
}
}
}
} else if (eventType == XmlPullParser.TEXT && inTown) {
li.town = xpp.getText();
}
if (eventType == XmlPullParser.END_TAG) {
inWoe = false;
inTown = false;
}
eventType = xpp.next();
}
// Add the primary woeid if it was found.
if (!TextUtils.isEmpty(primaryWoeid)) {
li.woeids.add(primaryWoeid);
}
// Sort by descending tag name to order by decreasing precision
// (locality3, locality2, locality1, admin3, admin2, admin1, etc.)
Collections.sort(alternateWoeids, new Comparator<Pair<String, String>>() {
@Override
public int compare(Pair<String, String> pair1, Pair<String, String> pair2) {
return pair1.first.compareTo(pair2.first);
}
});
for (Pair<String, String> pair : alternateWoeids) {
li.woeids.add(pair.second);
}
if (li.woeids.size() > 0) {
return li;
}
throw new CantGetWeatherException(true, R.string.no_weather_data,
"No WOEIDs found nearby.");
} catch (IOException e) {
throw new CantGetWeatherException(true, R.string.no_weather_data,
"Error parsing place search XML", e);
} catch (XmlPullParserException e) {
throw new CantGetWeatherException(true, R.string.no_weather_data,
"Error parsing place search XML", e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private static final int PARSE_STATE_NONE = 0;
private static final int PARSE_STATE_PLACE = 1;
private static final int PARSE_STATE_WOEID = 2;
private static final int PARSE_STATE_NAME = 3;
private static final int PARSE_STATE_COUNTRY = 4;
private static final int PARSE_STATE_ADMIN1 = 5;
public static List<LocationSearchResult> findLocationsAutocomplete(String startsWith) {
LOGD(TAG, "Autocompleting locations starting with '" + startsWith + "'");
List<LocationSearchResult> results = new ArrayList<LocationSearchResult>();
HttpURLConnection connection = null;
try {
connection = Utils.openUrlConnection(buildPlaceSearchStartsWithUrl(startsWith));
XmlPullParser xpp = sXmlPullParserFactory.newPullParser();
xpp.setInput(new InputStreamReader(connection.getInputStream()));
LocationSearchResult result = null;
String name = null, country = null, admin1 = null;
StringBuilder sb = new StringBuilder();
int state = PARSE_STATE_NONE;
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
String tagName = xpp.getName();
if (eventType == XmlPullParser.START_TAG) {
switch (state) {
case PARSE_STATE_NONE:
if ("place".equals(tagName)) {
state = PARSE_STATE_PLACE;
result = new LocationSearchResult();
name = country = admin1 = null;
}
break;
case PARSE_STATE_PLACE:
if ("name".equals(tagName)) {
state = PARSE_STATE_NAME;
} else if ("woeid".equals(tagName)) {
state = PARSE_STATE_WOEID;
} else if ("country".equals(tagName)) {
state = PARSE_STATE_COUNTRY;
} else if ("admin1".equals(tagName)) {
state = PARSE_STATE_ADMIN1;
}
break;
}
} else if (eventType == XmlPullParser.TEXT) {
switch (state) {
case PARSE_STATE_WOEID:
result.woeid = xpp.getText();
break;
case PARSE_STATE_NAME:
name = xpp.getText();
break;
case PARSE_STATE_COUNTRY:
country = xpp.getText();
break;
case PARSE_STATE_ADMIN1:
admin1 = xpp.getText();
break;
}
} else if (eventType == XmlPullParser.END_TAG) {
if ("place".equals(tagName)) {
// // Sort by descending tag name to order by decreasing precision
// // (locality3, locality2, locality1, admin3, admin2, admin1, etc.)
// Collections.sort(alternateWoeids, new Comparator<Pair<String, String>>() {
// @Override
// public int compare(Pair<String, String> pair1,
// Pair<String, String> pair2) {
// return pair1.first.compareTo(pair2.first);
// }
// });
sb.setLength(0);
if (!TextUtils.isEmpty(name)) {
sb.append(name);
}
if (!TextUtils.isEmpty(admin1)) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(admin1);
}
result.displayName = sb.toString();
result.country = country;
results.add(result);
state = PARSE_STATE_NONE;
} else if (state != PARSE_STATE_NONE) {
state = PARSE_STATE_PLACE;
}
}
eventType = xpp.next();
}
} catch (IOException e) {
LOGW(TAG, "Error parsing place search XML");
} catch (XmlPullParserException e) {
LOGW(TAG, "Error parsing place search XML");
} finally {
if (connection != null) {
connection.disconnect();
}
}
return results;
}
private static String buildWeatherQueryUrl(String woeid) {
// http://developer.yahoo.com/weather/
String query = "https://query.yahooapis.com/v1/public/yql?q="
+ "SELECT * FROM weather.forecast WHERE woeid=" + woeid
+ " and u=\'" + sWeatherUnits + "\'";
return query.replaceAll(" ", "%20");
}
private static String buildPlaceSearchUrl(Location l) {
// GeoPlanet API
return "http://where.yahooapis.com/v1/places.q('"
+ l.getLatitude() + "," + l.getLongitude() + "')"
+ "?appid=" + YahooWeatherApiConfig.APP_ID;
}
private static String buildPlaceSearchStartsWithUrl(String startsWith) {
// GeoPlanet API
startsWith = startsWith.replaceAll("[^\\w ]+", "").replaceAll(" ", "%20");
return "http://where.yahooapis.com/v1/places.q('" + startsWith + "%2A');"
+ "count=" + MAX_SEARCH_RESULTS
+ "?appid=" + YahooWeatherApiConfig.APP_ID;
}
public static class LocationInfo {
// Sorted by decreasing precision
// (point of interest, locality3, locality2, locality1, admin3, admin2, admin1, etc.)
List<String> woeids = new ArrayList<String>();
String town;
}
public static class LocationSearchResult {
String woeid;
String displayName;
String country;
}
}