1 package tim.prune.function.weather;
3 import java.awt.BorderLayout;
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;
11 import java.io.FileInputStream;
12 import java.io.InputStream;
14 import java.net.URLConnection;
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;
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;
44 * Function to display a weather forecast for the current location
45 * using the services of openweathermap.org
47 public class GetWeatherForecastFunction extends GenericFunction implements Runnable
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;
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;
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;
79 * Inner class to pass results asynchronously to the table model
81 private class ResultUpdater implements Runnable
83 private WeatherResults _results;
84 public ResultUpdater(WeatherResults inResults) {
88 _tableModel.setResults(_results);
95 public GetWeatherForecastFunction(App inApp)
100 /** @return name key */
101 public String getNameKey() {
102 return "function.getweatherforecast";
110 // Initialise dialog, show empty list
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());
122 _locationLabel.setText(I18nManager.getText("confirm.running"));
123 _updateTimeLabel.setText("");
124 _sunriseLabel.setText("");
125 _currentForecastRadio.setSelected(true);
127 // Start new thread to load list asynchronously
128 new Thread(this).start();
130 _dialog.setVisible(true);
134 * Create dialog components
135 * @return Panel containing all gui elements in dialog
137 private Component makeDialogComponents()
139 JPanel dialogPanel = new JPanel();
140 dialogPanel.setLayout(new BorderLayout(0, 4));
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();
172 _currentForecastRadio.addActionListener(radioListener);
173 _dailyForecastRadio.addActionListener(radioListener);
174 threeHourlyRadio.addActionListener(radioListener);
175 radioPanel.add(Box.createHorizontalGlue());
176 radioPanel.add(Box.createHorizontalStrut(40));
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")
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);
190 final IconRenderer iconRenderer = new IconRenderer();
191 _forecastsTable = new JTable(_tableModel)
193 public TableCellRenderer getCellRenderer(int row, int column) {
194 if ((row == WeatherTableModel.ROW_ICON)) {
197 return super.getCellRenderer(row, column);
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);
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);
212 dialogPanel.add(scroller, BorderLayout.CENTER);
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));
223 buttonPanel.add(launchButton);
225 JButton closeButton = new JButton(I18nManager.getText("button.close"));
226 closeButton.addActionListener(new ActionListener() {
227 public void actionPerformed(ActionEvent e) {
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));
243 * Get the weather forecast in a separate thread
247 if (_isRunning) {return;} // don't run twice
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;
256 // Have we got these results already? Look in store
257 WeatherResults results = _resultSet.getWeather(_locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
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);
271 // Get the specified forecast using the retrieved locationId
272 results = getWeatherForecast(isDailyForecast, isUsingCelsius);
274 // If it's a valid answer, store it for later
277 _resultSet.setWeather(results, _locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
281 // update table contents and labels
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)
290 _sunriseLabel.setText(I18nManager.getText("dialog.weather.sunrise") + ": " + results.getSunriseTime()
291 + ", " + I18nManager.getText("dialog.weather.sunset") + ": " + results.getSunsetTime());
294 _sunriseLabel.setText("");
304 * Adjust the column widths and row heights to fit the displayed data
306 private void adjustTable()
308 if (!_tableModel.isEmpty())
310 // adjust column widths for all columns
311 for (int i=0; i<_forecastsTable.getColumnCount(); i++)
313 double maxWidth = 0.0;
314 for (int j=0; j<_forecastsTable.getRowCount(); j++)
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());
320 _forecastsTable.getColumnModel().getColumn(i).setMinWidth((int) maxWidth + 2);
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++)
327 if (i == WeatherTableModel.ROW_ICON) {
328 _forecastsTable.setRowHeight(i, 55);
331 _forecastsTable.setRowHeight(i, labelHeight);
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
342 private WeatherResults getCurrentWeather(boolean inUseCelsius)
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)
352 // Use selected point
353 lat = currPoint.getLatitude().getDouble();
354 lon = currPoint.getLongitude().getDouble();
358 lat = track.getLatRange().getMidValue();
359 lon = track.getLonRange().getMidValue();
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);
371 // Parse the returned XML with a special handler
372 OWMCurrentHandler xmlHandler = new OWMCurrentHandler();
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)
380 inStream = new FileInputStream(new File("tim/prune/test/examplecurrentweather.xml"));
383 } catch (InterruptedException tie) {}
387 URLConnection conn = url.openConnection();
388 conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
389 inStream = conn.getInputStream();
392 saxParser.parse(inStream, xmlHandler);
396 // Show error message but don't close dialog
397 _app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
401 // Close stream and ignore errors
404 } catch (Exception e) {}
406 // Save the location id
407 if (xmlHandler.getLocationId() != null) {
408 _locationId = xmlHandler.getLocationId();
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);
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
427 private WeatherResults getWeatherForecast(boolean inDaily, boolean inCelsius)
429 InputStream inStream = null;
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);
437 // Parse the returned XML with a special handler
438 OWMForecastHandler xmlHandler = new OWMForecastHandler();
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)
446 inStream = new FileInputStream(new File("tim/prune/test/exampleweatherforecast.xml"));
449 } catch (InterruptedException tie) {}
453 URLConnection conn = url.openConnection();
454 conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
455 inStream = conn.getInputStream();
458 saxParser.parse(inStream, xmlHandler);
462 // Show error message but don't close dialog
463 _app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
467 // Close stream and ignore errors
470 } catch (Exception e) {}
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);