]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/function/weather/GetWeatherForecastFunction.java
Version 16, February 2014
[GpsPrune.git] / tim / prune / function / weather / GetWeatherForecastFunction.java
1 package tim.prune.function.weather;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Component;
6 import java.awt.Dimension;
7 import java.awt.FlowLayout;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.io.File;
11 import java.io.FileInputStream;
12 import java.io.InputStream;
13 import java.net.URL;
14 import java.net.URLConnection;
15
16 import javax.swing.BorderFactory;
17 import javax.swing.Box;
18 import javax.swing.BoxLayout;
19 import javax.swing.ButtonGroup;
20 import javax.swing.JButton;
21 import javax.swing.JComboBox;
22 import javax.swing.JDialog;
23 import javax.swing.JLabel;
24 import javax.swing.JPanel;
25 import javax.swing.JRadioButton;
26 import javax.swing.JScrollPane;
27 import javax.swing.JTable;
28 import javax.swing.ScrollPaneConstants;
29 import javax.swing.SwingUtilities;
30 import javax.swing.table.TableCellRenderer;
31 import javax.xml.parsers.SAXParser;
32 import javax.xml.parsers.SAXParserFactory;
33
34 import tim.prune.App;
35 import tim.prune.GenericFunction;
36 import tim.prune.GpsPrune;
37 import tim.prune.I18nManager;
38 import tim.prune.data.DataPoint;
39 import tim.prune.data.NumberUtils;
40 import tim.prune.data.Track;
41 import tim.prune.function.browser.BrowserLauncher;
42
43 /**
44  * Function to display a weather forecast for the current location
45  * using the services of openweathermap.org
46  */
47 public class GetWeatherForecastFunction extends GenericFunction implements Runnable
48 {
49         /** Dialog object */
50         private JDialog _dialog = null;
51         /** Label for location */
52         private JLabel _locationLabel = null;
53         /** Label for the forecast update time */
54         private JLabel _updateTimeLabel = null;
55         /** Label for the sunrise and sunset times */
56         private JLabel _sunriseLabel = null;
57         /** Radio button for selecting current weather */
58         private JRadioButton _currentForecastRadio = null;
59         /** Radio button for selecting daily forecasts */
60         private JRadioButton _dailyForecastRadio = null;
61         /** Dropdown for selecting celsius / fahrenheit */
62         private JComboBox<String> _tempUnitsDropdown = null;
63         /** Table to hold the forecasts */
64         private JTable _forecastsTable = null;
65         /** Table model */
66         private WeatherTableModel _tableModel = new WeatherTableModel();
67         /** Set of previously obtained results, to avoid repeating calls */
68         private ResultSet _resultSet = new ResultSet();
69         /** Location id obtained from current forecast */
70         private String _locationId = null;
71         /** Flag to show that forecast is currently running, don't start another */
72         private boolean _isRunning = false;
73
74         /** True to just simulate the calls and read files instead, false to call real API */
75         private static final boolean SIMULATE_WITH_FILES = false;
76
77
78         /**
79          * Inner class to pass results asynchronously to the table model
80          */
81         private class ResultUpdater implements Runnable
82         {
83                 private WeatherResults _results;
84                 public ResultUpdater(WeatherResults inResults) {
85                         _results = inResults;
86                 }
87                 public void run() {
88                         _tableModel.setResults(_results);
89                         adjustTable();
90                 }
91         }
92
93
94         /** Constructor */
95         public GetWeatherForecastFunction(App inApp)
96         {
97                 super(inApp);
98         }
99
100         /** @return name key */
101         public String getNameKey() {
102                 return "function.getweatherforecast";
103         }
104
105         /**
106          * Begin the function
107          */
108         public void begin()
109         {
110                 // Initialise dialog, show empty list
111                 if (_dialog == null)
112                 {
113                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
114                         _dialog.setLocationRelativeTo(_parentFrame);
115                         _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
116                         _dialog.getContentPane().add(makeDialogComponents());
117                         _dialog.pack();
118                 }
119                 // Clear results
120                 _locationId = null;
121                 _tableModel.clear();
122                 _locationLabel.setText(I18nManager.getText("confirm.running"));
123                 _updateTimeLabel.setText("");
124                 _sunriseLabel.setText("");
125                 _currentForecastRadio.setSelected(true);
126
127                 // Start new thread to load list asynchronously
128                 new Thread(this).start();
129
130                 _dialog.setVisible(true);
131         }
132
133         /**
134          * Create dialog components
135          * @return Panel containing all gui elements in dialog
136          */
137         private Component makeDialogComponents()
138         {
139                 JPanel dialogPanel = new JPanel();
140                 dialogPanel.setLayout(new BorderLayout(0, 4));
141
142                 JPanel topPanel = new JPanel();
143                 topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
144                 _locationLabel = new JLabel(I18nManager.getText("confirm.running"));
145                 _locationLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
146                 topPanel.add(_locationLabel);
147                 _updateTimeLabel = new JLabel(" ");
148                 _updateTimeLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
149                 topPanel.add(_updateTimeLabel);
150                 _sunriseLabel = new JLabel(" ");
151                 _sunriseLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
152                 topPanel.add(_sunriseLabel);
153                 JPanel radioPanel = new JPanel();
154                 radioPanel.setLayout(new BoxLayout(radioPanel, BoxLayout.X_AXIS));
155                 radioPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
156                 ButtonGroup forecastTypeGroup = new ButtonGroup();
157                 _currentForecastRadio = new JRadioButton(I18nManager.getText("dialog.weather.currentforecast"));
158                 _dailyForecastRadio = new JRadioButton(I18nManager.getText("dialog.weather.dailyforecast"));
159                 JRadioButton threeHourlyRadio = new JRadioButton(I18nManager.getText("dialog.weather.3hourlyforecast"));
160                 forecastTypeGroup.add(_currentForecastRadio);
161                 forecastTypeGroup.add(_dailyForecastRadio);
162                 forecastTypeGroup.add(threeHourlyRadio);
163                 radioPanel.add(_currentForecastRadio);
164                 radioPanel.add(_dailyForecastRadio);
165                 radioPanel.add(threeHourlyRadio);
166                 _currentForecastRadio.setSelected(true);
167                 ActionListener radioListener = new ActionListener() {
168                         public void actionPerformed(ActionEvent arg0) {
169                                 if (!_isRunning) new Thread(GetWeatherForecastFunction.this).start();
170                         }
171                 };
172                 _currentForecastRadio.addActionListener(radioListener);
173                 _dailyForecastRadio.addActionListener(radioListener);
174                 threeHourlyRadio.addActionListener(radioListener);
175                 radioPanel.add(Box.createHorizontalGlue());
176                 radioPanel.add(Box.createHorizontalStrut(40));
177
178                 // Dropdown for temperature units
179                 radioPanel.add(new JLabel(I18nManager.getText("dialog.weather.temperatureunits") + ": "));
180                 _tempUnitsDropdown = new JComboBox<String>(new String[] {
181                         I18nManager.getText("units.degreescelsius"), I18nManager.getText("units.degreesfahrenheit")
182                 });
183                 _tempUnitsDropdown.setMaximumSize(_tempUnitsDropdown.getPreferredSize());
184                 _tempUnitsDropdown.addActionListener(radioListener);
185                 radioPanel.add(_tempUnitsDropdown);
186                 radioPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
187                 topPanel.add(radioPanel);
188                 dialogPanel.add(topPanel, BorderLayout.NORTH);
189
190                 final IconRenderer iconRenderer = new IconRenderer();
191                 _forecastsTable = new JTable(_tableModel)
192                 {
193                         public TableCellRenderer getCellRenderer(int row, int column) {
194                                 if ((row == WeatherTableModel.ROW_ICON)) {
195                                         return iconRenderer;
196                                 }
197                                 return super.getCellRenderer(row, column);
198                         }
199                 };
200                 _forecastsTable.setRowSelectionAllowed(false);
201                 _forecastsTable.setRowHeight(2, 55); // make just that row high enough to see icons
202                 _forecastsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
203                 _forecastsTable.getTableHeader().setReorderingAllowed(false);
204                 _forecastsTable.setShowHorizontalLines(false);
205
206                 JScrollPane scroller = new JScrollPane(_forecastsTable);
207                 scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
208                 scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
209                 scroller.setPreferredSize(new Dimension(500, 210));
210                 scroller.getViewport().setBackground(Color.white);
211
212                 dialogPanel.add(scroller, BorderLayout.CENTER);
213
214                 // button panel at bottom
215                 JPanel buttonPanel = new JPanel();
216                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
217                 JButton launchButton = new JButton(I18nManager.getText("button.showwebpage"));
218                 launchButton.addActionListener(new ActionListener() {
219                         public void actionPerformed(ActionEvent arg0) {
220                                 BrowserLauncher.launchBrowser("http://openweathermap.org/city/" + (_locationId == null ? "" : _locationId));
221                         }
222                 });
223                 buttonPanel.add(launchButton);
224                 // close
225                 JButton closeButton = new JButton(I18nManager.getText("button.close"));
226                 closeButton.addActionListener(new ActionListener() {
227                         public void actionPerformed(ActionEvent e) {
228                                 _dialog.dispose();
229                         }
230                 });
231                 buttonPanel.add(closeButton);
232                 // Add a holder panel with a static label to credit openweathermap
233                 JPanel southPanel = new JPanel();
234                 southPanel.setLayout(new BoxLayout(southPanel, BoxLayout.Y_AXIS));
235                 southPanel.add(new JLabel(I18nManager.getText("dialog.weather.creditnotice")));
236                 southPanel.add(buttonPanel);
237                 dialogPanel.add(southPanel, BorderLayout.SOUTH);
238                 dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
239                 return dialogPanel;
240         }
241
242         /**
243          * Get the weather forecast in a separate thread
244          */
245         public void run()
246         {
247                 if (_isRunning) {return;} // don't run twice
248                 _isRunning = true;
249
250                 // Are we getting the current details, or getting a forecast?
251                 final boolean isCurrent = _locationId == null || _currentForecastRadio.isSelected();
252                 final boolean isDailyForecast = _dailyForecastRadio.isSelected() && !isCurrent;
253                 final boolean isHourlyForecast = !isCurrent && !isDailyForecast;
254                 final boolean isUsingCelsius  = _tempUnitsDropdown.getSelectedIndex() == 0;
255
256                 // Have we got these results already?  Look in store
257                 WeatherResults results = _resultSet.getWeather(_locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
258                 if (results == null)
259                 {
260                         if (isCurrent)
261                         {
262                                 // Get the current details using either lat/long or locationId
263                                 results = getCurrentWeather(isUsingCelsius);
264                                 // If the current radio isn't selected, select it
265                                 if (!_currentForecastRadio.isSelected()) {
266                                         _currentForecastRadio.setSelected(true);
267                                 }
268                         }
269                         else
270                         {
271                                 // Get the specified forecast using the retrieved locationId
272                                 results = getWeatherForecast(isDailyForecast, isUsingCelsius);
273                         }
274                         // If it's a valid answer, store it for later
275                         if (results != null)
276                         {
277                                 _resultSet.setWeather(results, _locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
278                         }
279                 }
280
281                 // update table contents and labels
282                 if (results != null)
283                 {
284                         SwingUtilities.invokeLater(new ResultUpdater(results));
285                         _locationLabel.setText(I18nManager.getText("dialog.weather.location") + ": " + results.getLocationName());
286                         final String ut = results.getUpdateTime();
287                         _updateTimeLabel.setText(I18nManager.getText("dialog.weather.update") + ": " + (ut == null ? "" : ut));
288                         if (results.getSunriseTime() != null && results.getSunsetTime() != null)
289                         {
290                                 _sunriseLabel.setText(I18nManager.getText("dialog.weather.sunrise") + ": " + results.getSunriseTime()
291                                         + ", " + I18nManager.getText("dialog.weather.sunset") + ": " + results.getSunsetTime());
292                         }
293                         else {
294                                 _sunriseLabel.setText("");
295                         }
296                 }
297
298                 // finished running
299                 _isRunning = false;
300         }
301
302
303         /**
304          * Adjust the column widths and row heights to fit the displayed data
305          */
306         private void adjustTable()
307         {
308                 if (!_tableModel.isEmpty())
309                 {
310                         // adjust column widths for all columns
311                         for (int i=0; i<_forecastsTable.getColumnCount(); i++)
312                         {
313                                 double maxWidth = 0.0;
314                                 for (int j=0; j<_forecastsTable.getRowCount(); j++)
315                                 {
316                                         final String value = _tableModel.getValueAt(j, i).toString();
317                                         maxWidth = Math.max(maxWidth, _forecastsTable.getCellRenderer(0, 0).getTableCellRendererComponent(
318                                                 _forecastsTable, value, false, false, 0, 0).getPreferredSize().getWidth());
319                                 }
320                                 _forecastsTable.getColumnModel().getColumn(i).setMinWidth((int) maxWidth + 2);
321                         }
322                         // Set minimum row heights
323                         final int labelHeight = (int) (_forecastsTable.getCellRenderer(0, 0).getTableCellRendererComponent(
324                                 _forecastsTable, "M", false, false, 0, 0).getMinimumSize().getHeight() * 1.2f + 4);
325                         for (int i=0; i<_forecastsTable.getRowCount(); i++)
326                         {
327                                 if (i == WeatherTableModel.ROW_ICON) {
328                                         _forecastsTable.setRowHeight(i, 55);
329                                 }
330                                 else {
331                                         _forecastsTable.setRowHeight(i, labelHeight);
332                                 }
333                         }
334                 }
335         }
336
337         /**
338          * Get the current weather using the lat/long and populate _results
339          * @param inUseCelsius true for celsius, false for fahrenheit
340          * @return weather results
341          */
342         private WeatherResults getCurrentWeather(boolean inUseCelsius)
343         {
344                 final Track track = _app.getTrackInfo().getTrack();
345                 if (track.getNumPoints() < 1) {return null;}
346                 // Get coordinates to lookup
347                 double lat = 0.0, lon = 0.0;
348                 // See if a point is selected, if so use that
349                 DataPoint currPoint = _app.getTrackInfo().getCurrentPoint();
350                 if (currPoint != null)
351                 {
352                         // Use selected point
353                         lat = currPoint.getLatitude().getDouble();
354                         lon = currPoint.getLongitude().getDouble();
355                 }
356                 else
357                 {
358                         lat = track.getLatRange().getMidValue();
359                         lon = track.getLonRange().getMidValue();
360                 }
361
362                 InputStream inStream = null;
363                 // Build url either with coordinates or with location id if available
364                 final String urlString = "http://api.openweathermap.org/data/2.5/weather?"
365                         + (_locationId == null ? ("lat=" + NumberUtils.formatNumberUk(lat, 5) + "&lon=" + NumberUtils.formatNumberUk(lon, 5))
366                                 : ("id=" + _locationId))
367                         + "&lang=" + I18nManager.getText("openweathermap.lang")
368                         + "&mode=xml&units=" + (inUseCelsius ? "metric" : "imperial");
369                 // System.out.println(urlString);
370
371                 // Parse the returned XML with a special handler
372                 OWMCurrentHandler xmlHandler = new OWMCurrentHandler();
373                 try
374                 {
375                         URL url = new URL(urlString);
376                         SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
377                         // DEBUG: Simulate the call in case of no network connection
378                         if (SIMULATE_WITH_FILES)
379                         {
380                                 inStream = new FileInputStream(new File("tim/prune/test/examplecurrentweather.xml"));
381                                 try {
382                                         Thread.sleep(2000);
383                                 } catch (InterruptedException tie) {}
384                         }
385                         else
386                         {
387                                 URLConnection conn = url.openConnection();
388                                 conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
389                                 inStream = conn.getInputStream();
390                         }
391
392                         saxParser.parse(inStream, xmlHandler);
393                 }
394                 catch (Exception e)
395                 {
396                         // Show error message but don't close dialog
397                         _app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
398                         _isRunning = false;
399                         return null;
400                 }
401                 // Close stream and ignore errors
402                 try {
403                         inStream.close();
404                 } catch (Exception e) {}
405
406                 // Save the location id
407                 if (xmlHandler.getLocationId() != null) {
408                         _locationId = xmlHandler.getLocationId();
409                 }
410                 // Get the results from the handler and return
411                 WeatherResults results = new WeatherResults();
412                 results.setForecast(xmlHandler.getCurrentWeather());
413                 results.setLocationName(xmlHandler.getLocationName());
414                 results.setUpdateTime(xmlHandler.getUpdateTime());
415                 results.setSunriseSunsetTimes(xmlHandler.getSunriseTime(), xmlHandler.getSunsetTime());
416                 results.setTempsCelsius(inUseCelsius);
417                 return results;
418         }
419
420
421         /**
422          * Get the weather forecast for the current location id and populate in _results
423          * @param inDaily true for daily, false for 3-hourly
424          * @param inCelsius true for celsius, false for fahrenheit
425          * @return weather results
426          */
427         private WeatherResults getWeatherForecast(boolean inDaily, boolean inCelsius)
428         {
429                 InputStream inStream = null;
430                 // Build URL
431                 final String forecastCount = inDaily ? "8" : "3";
432                 final String urlString = "http://api.openweathermap.org/data/2.5/forecast"
433                         + (inDaily ? "/daily" : "") + "?id=" + _locationId + "&lang=" + I18nManager.getText("openweathermap.lang")
434                         + "&mode=xml&units=" + (inCelsius ? "metric" : "imperial") + "&cnt=" + forecastCount;
435                 // System.out.println(urlString);
436
437                 // Parse the returned XML with a special handler
438                 OWMForecastHandler xmlHandler = new OWMForecastHandler();
439                 try
440                 {
441                         URL url = new URL(urlString);
442                         SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
443                         // DEBUG: Simulate the call in case of no network connection
444                         if (SIMULATE_WITH_FILES)
445                         {
446                                 inStream = new FileInputStream(new File("tim/prune/test/exampleweatherforecast.xml"));
447                                 try {
448                                         Thread.sleep(2000);
449                                 } catch (InterruptedException tie) {}
450                         }
451                         else
452                         {
453                                 URLConnection conn = url.openConnection();
454                                 conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
455                                 inStream = conn.getInputStream();
456                         }
457
458                         saxParser.parse(inStream, xmlHandler);
459                 }
460                 catch (Exception e)
461                 {
462                         // Show error message but don't close dialog
463                         _app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
464                         _isRunning = false;
465                         return null;
466                 }
467                 // Close stream and ignore errors
468                 try {
469                         inStream.close();
470                 } catch (Exception e) {}
471
472                 // Get results from handler, put in model
473                 WeatherResults results = new WeatherResults();
474                 results.setForecasts(xmlHandler.getForecasts());
475                 results.setLocationName(xmlHandler.getLocationName());
476                 results.setUpdateTime(xmlHandler.getUpdateTime());
477                 results.setTempsCelsius(inCelsius);
478                 return results;
479         }
480 }