/Sky/src/org/jsharkey/sky/webservice/NoaaSource.java

http://android-sky.googlecode.com/ · Java · 262 lines · 161 code · 40 blank · 61 comment · 43 complexity · 2d580514feb833c918b938b4e70e0f8a MD5 · raw file

  1. /*
  2. * Copyright (C) 2009 Jeff Sharkey, http://jsharkey.org/
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.jsharkey.sky.webservice;
  17. import java.io.IOException;
  18. import java.io.Reader;
  19. import java.util.ArrayList;
  20. import java.util.Collections;
  21. import java.util.Comparator;
  22. import java.util.HashMap;
  23. import java.util.List;
  24. import java.util.Map;
  25. import org.jsharkey.sky.ForecastProvider;
  26. import org.jsharkey.sky.webservice.Forecast.ParseException;
  27. import org.xmlpull.v1.XmlPullParser;
  28. import org.xmlpull.v1.XmlPullParserException;
  29. import org.xmlpull.v1.XmlPullParserFactory;
  30. import android.text.Editable;
  31. import android.text.SpannableStringBuilder;
  32. import android.text.TextUtils;
  33. import android.text.format.Time;
  34. import android.util.Log;
  35. import android.util.TimeFormatException;
  36. /**
  37. * Helper class to handle querying a webservice for forecast details and parsing
  38. * results into {@link ForecastProvider}.
  39. */
  40. public class NoaaSource implements ForecastSource {
  41. private static final String TAG = "NoaaHelper";
  42. static final String WEBSERVICE_URL = "http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?&lat=%f&lon=%f&format=24+hourly&numDays=%d";
  43. /**
  44. * Various XML tags present in the response.
  45. */
  46. private static final String TAG_TEMPERATURE = "temperature";
  47. private static final String TAG_WEATHER = "weather";
  48. private static final String TAG_POP = "probability-of-precipitation";
  49. private static final String TAG_HAZARDS = "hazards";
  50. private static final String TAG_WEATHER_CONDITIONS = "weather-conditions";
  51. private static final String TAG_HAZARD = "hazard";
  52. private static final String TAG_LAYOUT_KEY = "layout-key";
  53. private static final String TAG_START_VALID_TIME = "start-valid-time";
  54. private static final String TAG_VALUE = "value";
  55. private static final String TAG_HAZARDTEXTURL = "hazardTextURL";
  56. private static final String TAG_MOREWEATHERINFORMATION = "moreWeatherInformation";
  57. /**
  58. * Various XML attributes present in the response.
  59. */
  60. private static final String ATTR_TIME_LAYOUT = "time-layout";
  61. private static final String ATTR_TYPE = "type";
  62. private static final String ATTR_WEATHER_SUMMARY = "weather-summary";
  63. private static final String ATTR_PHENOMENA = "phenomena";
  64. private static final String ATTR_SIGNIFICANCE = "significance";
  65. private static final String TYPE_MAXIMUM = "maximum";
  66. private static final String TYPE_MINIMUM = "minimum";
  67. /**
  68. * Recycled string builder used by {@link #parseDate(String)}.
  69. */
  70. private static Editable sEditable = new SpannableStringBuilder();
  71. /**
  72. * Recycled timestamp used by {@link #parseDate(String)}.
  73. */
  74. private static Time sTime = new Time();
  75. private static XmlPullParserFactory sFactory = null;
  76. /**
  77. * Parse a NWS date string into a Unix timestamp. Assumes incoming values
  78. * are in the format "2009-03-23T18:00:00-07:00", which we adjust slightly
  79. * to correctly follow RFC 3339 before parsing.
  80. */
  81. private static long parseDate(String raw) throws TimeFormatException {
  82. // Inject milliseconds so that NWS dates follow RFC
  83. sEditable.clear();
  84. sEditable.append(raw);
  85. sEditable.insert(19, ".000");
  86. String rfcFormat = sEditable.toString();
  87. sTime.parse3339(rfcFormat);
  88. return sTime.toMillis(false);
  89. }
  90. /**
  91. * Retrieve a specific {@link Forecast} object from the given {@link Map}
  92. * structure. If the {@link Forecast} doesn't exist, it's created and
  93. * returned.
  94. */
  95. private static Forecast getForecast(Map<String, List<Forecast>> forecasts,
  96. String layout, int index) {
  97. if (!forecasts.containsKey(layout)) {
  98. forecasts.put(layout, new ArrayList<Forecast>());
  99. }
  100. List<Forecast> layoutSpecific = forecasts.get(layout);
  101. while (index >= layoutSpecific.size()) {
  102. layoutSpecific.add(new Forecast());
  103. }
  104. return layoutSpecific.get(index);
  105. }
  106. /**
  107. * Flatten a set of {@link Forecast} objects that are separated into
  108. * <code>time-layout</code> sections in the given {@link Map}. This discards
  109. * any forecasts that have empty {@link Forecast#conditions}.
  110. * <p>
  111. * Sorts the resulting list by time, with any alerts forced to the top.
  112. */
  113. private static List<Forecast> flattenForecasts(Map<String, List<Forecast>> forecasts) {
  114. List<Forecast> flat = new ArrayList<Forecast>();
  115. // Collect together all forecasts that have valid conditions
  116. for (String layout : forecasts.keySet()) {
  117. for (Forecast forecast : forecasts.get(layout)) {
  118. if (!TextUtils.isEmpty(forecast.conditions)) {
  119. flat.add(forecast);
  120. }
  121. }
  122. }
  123. // Sort by time, but always bump alerts to top
  124. Collections.sort(flat, new Comparator<Forecast>() {
  125. public int compare(Forecast left, Forecast right) {
  126. if (left.alert) {
  127. return -1;
  128. } else {
  129. return (int)(left.validStart - right.validStart);
  130. }
  131. }
  132. });
  133. return flat;
  134. }
  135. /**
  136. * {@inheritDoc}
  137. */
  138. @Override
  139. public List<Forecast> getForecasts(double lat, double lon, int days) throws ParseException {
  140. if (Double.isNaN(lat) || Double.isNaN(lon)) {
  141. throw new ParseException("Requested forecast for invalid location");
  142. } else {
  143. Log.d(TAG, String.format("queryLocation() with lat=%f, lon=%f, days=%d", lat, lon, days));
  144. }
  145. // Make API call to find forecasts
  146. String url = String.format(WEBSERVICE_URL, lat, lon, days);
  147. Reader reader = WebserviceHelper.queryApi(url);
  148. // Parse incoming forecast data
  149. List<Forecast> forecasts = parseResponse(reader);
  150. return forecasts;
  151. }
  152. /**
  153. * Parse a webservice XML response into {@link Forecast} objects.
  154. */
  155. private static List<Forecast> parseResponse(Reader response) throws ParseException {
  156. // Keep a temporary mapping between time series tags and forecasts
  157. Map<String, List<Forecast>> forecasts = new HashMap<String, List<Forecast>>();
  158. String detailsUrl = null;
  159. try {
  160. if (sFactory == null) {
  161. sFactory = XmlPullParserFactory.newInstance();
  162. }
  163. XmlPullParser xpp = sFactory.newPullParser();
  164. int index = 0;
  165. String thisTag = null;
  166. String thisLayout = null;
  167. String thisType = null;
  168. xpp.setInput(response);
  169. int eventType = xpp.getEventType();
  170. while (eventType != XmlPullParser.END_DOCUMENT) {
  171. if (eventType == XmlPullParser.START_TAG) {
  172. thisTag = xpp.getName();
  173. if (TAG_TEMPERATURE.equals(thisTag) || TAG_WEATHER.equals(thisTag)
  174. || TAG_POP.equals(thisTag) || TAG_HAZARDS.equals(thisTag)) {
  175. thisLayout = xpp.getAttributeValue(null, ATTR_TIME_LAYOUT);
  176. thisType = xpp.getAttributeValue(null, ATTR_TYPE);
  177. index = -1;
  178. } else if (TAG_WEATHER_CONDITIONS.equals(thisTag)) {
  179. Forecast forecast = getForecast(forecasts, thisLayout, ++index);
  180. forecast.conditions = xpp.getAttributeValue(null, ATTR_WEATHER_SUMMARY);
  181. } else if (TAG_HAZARD.equals(thisTag)) {
  182. Forecast forecast = getForecast(forecasts, thisLayout, ++index);
  183. forecast.alert = true;
  184. forecast.conditions = xpp.getAttributeValue(null, ATTR_PHENOMENA) + " "
  185. + xpp.getAttributeValue(null, ATTR_SIGNIFICANCE);
  186. }
  187. } else if (eventType == XmlPullParser.END_TAG) {
  188. thisTag = null;
  189. } else if (eventType == XmlPullParser.TEXT) {
  190. if (TAG_LAYOUT_KEY.equals(thisTag)) {
  191. thisLayout = xpp.getText();
  192. index = -1;
  193. } else if (TAG_START_VALID_TIME.equals(thisTag)) {
  194. Forecast forecast = getForecast(forecasts, thisLayout, ++index);
  195. forecast.validStart = parseDate(xpp.getText());
  196. } else if (TAG_VALUE.equals(thisTag) && TYPE_MAXIMUM.equals(thisType)) {
  197. Forecast forecast = getForecast(forecasts, thisLayout, ++index);
  198. forecast.tempHigh = Integer.parseInt(xpp.getText());
  199. forecast.url = detailsUrl;
  200. } else if (TAG_VALUE.equals(thisTag) && TYPE_MINIMUM.equals(thisType)) {
  201. Forecast forecast = getForecast(forecasts, thisLayout, ++index);
  202. forecast.tempLow = Integer.parseInt(xpp.getText());
  203. } else if (TAG_HAZARDTEXTURL.equals(thisTag)) {
  204. Forecast forecast = getForecast(forecasts, thisLayout, index);
  205. forecast.url = xpp.getText();
  206. } else if (TAG_MOREWEATHERINFORMATION.equals(thisTag)) {
  207. detailsUrl = xpp.getText();
  208. }
  209. }
  210. eventType = xpp.next();
  211. }
  212. } catch (IOException e) {
  213. throw new ParseException("Problem parsing XML forecast", e);
  214. } catch (XmlPullParserException e) {
  215. throw new ParseException("Problem parsing XML forecast", e);
  216. } catch (TimeFormatException e) {
  217. throw new ParseException("Problem parsing XML forecast", e);
  218. }
  219. // Flatten non-empty forecasts into single list
  220. return flattenForecasts(forecasts);
  221. }
  222. }