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;
76 /** Unique API key for GpsPrune */
77 private static final String OPENWEATHERMAP_API_KEY = "d1c5d792362f5a5c2eacf70a3b72ecd6";
81 * Inner class to pass results asynchronously to the table model
83 private class ResultUpdater implements Runnable
85 private WeatherResults _results;
86 public ResultUpdater(WeatherResults inResults) {
90 _tableModel.setResults(_results);
97 public GetWeatherForecastFunction(App inApp)
102 /** @return name key */
103 public String getNameKey() {
104 return "function.getweatherforecast";
112 // Initialise dialog, show empty list
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());
124 _locationLabel.setText(I18nManager.getText("confirm.running"));
125 _updateTimeLabel.setText("");
126 _sunriseLabel.setText("");
127 _currentForecastRadio.setSelected(true);
129 // Start new thread to load list asynchronously
130 new Thread(this).start();
132 _dialog.setVisible(true);
136 * Create dialog components
137 * @return Panel containing all gui elements in dialog
139 private Component makeDialogComponents()
141 JPanel dialogPanel = new JPanel();
142 dialogPanel.setLayout(new BorderLayout(0, 4));
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();
174 _currentForecastRadio.addActionListener(radioListener);
175 _dailyForecastRadio.addActionListener(radioListener);
176 threeHourlyRadio.addActionListener(radioListener);
177 radioPanel.add(Box.createHorizontalGlue());
178 radioPanel.add(Box.createHorizontalStrut(40));
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")
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);
192 final IconRenderer iconRenderer = new IconRenderer();
193 _forecastsTable = new JTable(_tableModel)
195 public TableCellRenderer getCellRenderer(int row, int column) {
196 if ((row == WeatherTableModel.ROW_ICON)) {
199 return super.getCellRenderer(row, column);
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);
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);
214 dialogPanel.add(scroller, BorderLayout.CENTER);
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));
225 buttonPanel.add(launchButton);
227 JButton closeButton = new JButton(I18nManager.getText("button.close"));
228 closeButton.addActionListener(new ActionListener() {
229 public void actionPerformed(ActionEvent e) {
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));
245 * Get the weather forecast in a separate thread
249 if (_isRunning) {return;} // don't run twice
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;
258 // Have we got these results already? Look in store
259 WeatherResults results = _resultSet.getWeather(_locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
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);
273 // Get the specified forecast using the retrieved locationId
274 results = getWeatherForecast(isDailyForecast, isUsingCelsius);
276 // If it's a valid answer, store it for later
279 _resultSet.setWeather(results, _locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
283 // update table contents and labels
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)
292 _sunriseLabel.setText(I18nManager.getText("dialog.weather.sunrise") + ": " + results.getSunriseTime()
293 + ", " + I18nManager.getText("dialog.weather.sunset") + ": " + results.getSunsetTime());
296 _sunriseLabel.setText("");
306 * Adjust the column widths and row heights to fit the displayed data
308 private void adjustTable()
310 if (!_tableModel.isEmpty())
312 // adjust column widths for all columns
313 for (int i=0; i<_forecastsTable.getColumnCount(); i++)
315 double maxWidth = 0.0;
316 for (int j=0; j<_forecastsTable.getRowCount(); j++)
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());
322 _forecastsTable.getColumnModel().getColumn(i).setMinWidth((int) maxWidth + 2);
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++)
329 if (i == WeatherTableModel.ROW_ICON) {
330 _forecastsTable.setRowHeight(i, 55);
333 _forecastsTable.setRowHeight(i, labelHeight);
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
344 private WeatherResults getCurrentWeather(boolean inUseCelsius)
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)
354 // Use selected point
355 lat = currPoint.getLatitude().getDouble();
356 lon = currPoint.getLongitude().getDouble();
360 lat = track.getLatRange().getMidValue();
361 lon = track.getLonRange().getMidValue();
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);
374 // Parse the returned XML with a special handler
375 OWMCurrentHandler xmlHandler = new OWMCurrentHandler();
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)
383 inStream = new FileInputStream(new File("tim/prune/test/examplecurrentweather.xml"));
386 } catch (InterruptedException tie) {}
390 URLConnection conn = url.openConnection();
391 conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
392 inStream = conn.getInputStream();
395 saxParser.parse(inStream, xmlHandler);
399 // Show error message but don't close dialog
400 _app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
404 // Close stream and ignore errors
407 } catch (Exception e) {}
409 // Save the location id
410 if (xmlHandler.getLocationId() != null) {
411 _locationId = xmlHandler.getLocationId();
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);
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
430 private WeatherResults getWeatherForecast(boolean inDaily, boolean inCelsius)
432 InputStream inStream = null;
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);
441 // Parse the returned XML with a special handler
442 OWMForecastHandler xmlHandler = new OWMForecastHandler();
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)
450 inStream = new FileInputStream(new File("tim/prune/test/exampleweatherforecast.xml"));
453 } catch (InterruptedException tie) {}
457 URLConnection conn = url.openConnection();
458 conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
459 inStream = conn.getInputStream();
462 saxParser.parse(inStream, xmlHandler);
466 // Show error message but don't close dialog
467 _app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
471 // Close stream and ignore errors
474 } catch (Exception e) {}
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);