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