PageRenderTime 166ms CodeModel.GetById 117ms app.highlight 43ms RepoModel.GetById 1ms app.codeStats 0ms

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