+package tim.prune.function.weather;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.table.TableCellRenderer;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import tim.prune.App;
+import tim.prune.GenericFunction;
+import tim.prune.GpsPrune;
+import tim.prune.I18nManager;
+import tim.prune.data.DataPoint;
+import tim.prune.data.NumberUtils;
+import tim.prune.data.Track;
+import tim.prune.function.browser.BrowserLauncher;
+
+/**
+ * Function to display a weather forecast for the current location
+ * using the services of openweathermap.org
+ */
+public class GetWeatherForecastFunction extends GenericFunction implements Runnable
+{
+ /** Dialog object */
+ private JDialog _dialog = null;
+ /** Label for location */
+ private JLabel _locationLabel = null;
+ /** Label for the forecast update time */
+ private JLabel _updateTimeLabel = null;
+ /** Label for the sunrise and sunset times */
+ private JLabel _sunriseLabel = null;
+ /** Radio button for selecting current weather */
+ private JRadioButton _currentForecastRadio = null;
+ /** Radio button for selecting daily forecasts */
+ private JRadioButton _dailyForecastRadio = null;
+ /** Dropdown for selecting celsius / fahrenheit */
+ private JComboBox<String> _tempUnitsDropdown = null;
+ /** Table to hold the forecasts */
+ private JTable _forecastsTable = null;
+ /** Table model */
+ private WeatherTableModel _tableModel = new WeatherTableModel();
+ /** Set of previously obtained results, to avoid repeating calls */
+ private ResultSet _resultSet = new ResultSet();
+ /** Location id obtained from current forecast */
+ private String _locationId = null;
+ /** Flag to show that forecast is currently running, don't start another */
+ private boolean _isRunning = false;
+
+ /** True to just simulate the calls and read files instead, false to call real API */
+ private static final boolean SIMULATE_WITH_FILES = false;
+
+
+ /**
+ * Inner class to pass results asynchronously to the table model
+ */
+ private class ResultUpdater implements Runnable
+ {
+ private WeatherResults _results;
+ public ResultUpdater(WeatherResults inResults) {
+ _results = inResults;
+ }
+ public void run() {
+ _tableModel.setResults(_results);
+ adjustTable();
+ }
+ }
+
+
+ /** Constructor */
+ public GetWeatherForecastFunction(App inApp)
+ {
+ super(inApp);
+ }
+
+ /** @return name key */
+ public String getNameKey() {
+ return "function.getweatherforecast";
+ }
+
+ /**
+ * Begin the function
+ */
+ public void begin()
+ {
+ // Initialise dialog, show empty list
+ if (_dialog == null)
+ {
+ _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
+ _dialog.setLocationRelativeTo(_parentFrame);
+ _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ _dialog.getContentPane().add(makeDialogComponents());
+ _dialog.pack();
+ }
+ // Clear results
+ _locationId = null;
+ _tableModel.clear();
+ _locationLabel.setText(I18nManager.getText("confirm.running"));
+ _updateTimeLabel.setText("");
+ _sunriseLabel.setText("");
+ _currentForecastRadio.setSelected(true);
+
+ // Start new thread to load list asynchronously
+ new Thread(this).start();
+
+ _dialog.setVisible(true);
+ }
+
+ /**
+ * Create dialog components
+ * @return Panel containing all gui elements in dialog
+ */
+ private Component makeDialogComponents()
+ {
+ JPanel dialogPanel = new JPanel();
+ dialogPanel.setLayout(new BorderLayout(0, 4));
+
+ JPanel topPanel = new JPanel();
+ topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
+ _locationLabel = new JLabel(I18nManager.getText("confirm.running"));
+ _locationLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
+ topPanel.add(_locationLabel);
+ _updateTimeLabel = new JLabel(" ");
+ _updateTimeLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
+ topPanel.add(_updateTimeLabel);
+ _sunriseLabel = new JLabel(" ");
+ _sunriseLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
+ topPanel.add(_sunriseLabel);
+ JPanel radioPanel = new JPanel();
+ radioPanel.setLayout(new BoxLayout(radioPanel, BoxLayout.X_AXIS));
+ radioPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
+ ButtonGroup forecastTypeGroup = new ButtonGroup();
+ _currentForecastRadio = new JRadioButton(I18nManager.getText("dialog.weather.currentforecast"));
+ _dailyForecastRadio = new JRadioButton(I18nManager.getText("dialog.weather.dailyforecast"));
+ JRadioButton threeHourlyRadio = new JRadioButton(I18nManager.getText("dialog.weather.3hourlyforecast"));
+ forecastTypeGroup.add(_currentForecastRadio);
+ forecastTypeGroup.add(_dailyForecastRadio);
+ forecastTypeGroup.add(threeHourlyRadio);
+ radioPanel.add(_currentForecastRadio);
+ radioPanel.add(_dailyForecastRadio);
+ radioPanel.add(threeHourlyRadio);
+ _currentForecastRadio.setSelected(true);
+ ActionListener radioListener = new ActionListener() {
+ public void actionPerformed(ActionEvent arg0) {
+ if (!_isRunning) new Thread(GetWeatherForecastFunction.this).start();
+ }
+ };
+ _currentForecastRadio.addActionListener(radioListener);
+ _dailyForecastRadio.addActionListener(radioListener);
+ threeHourlyRadio.addActionListener(radioListener);
+ radioPanel.add(Box.createHorizontalGlue());
+ radioPanel.add(Box.createHorizontalStrut(40));
+
+ // Dropdown for temperature units
+ radioPanel.add(new JLabel(I18nManager.getText("dialog.weather.temperatureunits") + ": "));
+ _tempUnitsDropdown = new JComboBox<String>(new String[] {
+ I18nManager.getText("units.degreescelsius"), I18nManager.getText("units.degreesfahrenheit")
+ });
+ _tempUnitsDropdown.setMaximumSize(_tempUnitsDropdown.getPreferredSize());
+ _tempUnitsDropdown.addActionListener(radioListener);
+ radioPanel.add(_tempUnitsDropdown);
+ radioPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
+ topPanel.add(radioPanel);
+ dialogPanel.add(topPanel, BorderLayout.NORTH);
+
+ final IconRenderer iconRenderer = new IconRenderer();
+ _forecastsTable = new JTable(_tableModel)
+ {
+ public TableCellRenderer getCellRenderer(int row, int column) {
+ if ((row == WeatherTableModel.ROW_ICON)) {
+ return iconRenderer;
+ }
+ return super.getCellRenderer(row, column);
+ }
+ };
+ _forecastsTable.setRowSelectionAllowed(false);
+ _forecastsTable.setRowHeight(2, 55); // make just that row high enough to see icons
+ _forecastsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+ _forecastsTable.getTableHeader().setReorderingAllowed(false);
+ _forecastsTable.setShowHorizontalLines(false);
+
+ JScrollPane scroller = new JScrollPane(_forecastsTable);
+ scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
+ scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
+ scroller.setPreferredSize(new Dimension(500, 210));
+ scroller.getViewport().setBackground(Color.white);
+
+ dialogPanel.add(scroller, BorderLayout.CENTER);
+
+ // button panel at bottom
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
+ JButton launchButton = new JButton(I18nManager.getText("button.showwebpage"));
+ launchButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent arg0) {
+ BrowserLauncher.launchBrowser("http://openweathermap.org/city/" + (_locationId == null ? "" : _locationId));
+ }
+ });
+ buttonPanel.add(launchButton);
+ // close
+ JButton closeButton = new JButton(I18nManager.getText("button.close"));
+ closeButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ _dialog.dispose();
+ }
+ });
+ buttonPanel.add(closeButton);
+ // Add a holder panel with a static label to credit openweathermap
+ JPanel southPanel = new JPanel();
+ southPanel.setLayout(new BoxLayout(southPanel, BoxLayout.Y_AXIS));
+ southPanel.add(new JLabel(I18nManager.getText("dialog.weather.creditnotice")));
+ southPanel.add(buttonPanel);
+ dialogPanel.add(southPanel, BorderLayout.SOUTH);
+ dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
+ return dialogPanel;
+ }
+
+ /**
+ * Get the weather forecast in a separate thread
+ */
+ public void run()
+ {
+ if (_isRunning) {return;} // don't run twice
+ _isRunning = true;
+
+ // Are we getting the current details, or getting a forecast?
+ final boolean isCurrent = _locationId == null || _currentForecastRadio.isSelected();
+ final boolean isDailyForecast = _dailyForecastRadio.isSelected() && !isCurrent;
+ final boolean isHourlyForecast = !isCurrent && !isDailyForecast;
+ final boolean isUsingCelsius = _tempUnitsDropdown.getSelectedIndex() == 0;
+
+ // Have we got these results already? Look in store
+ WeatherResults results = _resultSet.getWeather(_locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
+ if (results == null)
+ {
+ if (isCurrent)
+ {
+ // Get the current details using either lat/long or locationId
+ results = getCurrentWeather(isUsingCelsius);
+ // If the current radio isn't selected, select it
+ if (!_currentForecastRadio.isSelected()) {
+ _currentForecastRadio.setSelected(true);
+ }
+ }
+ else
+ {
+ // Get the specified forecast using the retrieved locationId
+ results = getWeatherForecast(isDailyForecast, isUsingCelsius);
+ }
+ // If it's a valid answer, store it for later
+ if (results != null)
+ {
+ _resultSet.setWeather(results, _locationId, isCurrent, isDailyForecast, isHourlyForecast, isUsingCelsius);
+ }
+ }
+
+ // update table contents and labels
+ if (results != null)
+ {
+ SwingUtilities.invokeLater(new ResultUpdater(results));
+ _locationLabel.setText(I18nManager.getText("dialog.weather.location") + ": " + results.getLocationName());
+ final String ut = results.getUpdateTime();
+ _updateTimeLabel.setText(I18nManager.getText("dialog.weather.update") + ": " + (ut == null ? "" : ut));
+ if (results.getSunriseTime() != null && results.getSunsetTime() != null)
+ {
+ _sunriseLabel.setText(I18nManager.getText("dialog.weather.sunrise") + ": " + results.getSunriseTime()
+ + ", " + I18nManager.getText("dialog.weather.sunset") + ": " + results.getSunsetTime());
+ }
+ else {
+ _sunriseLabel.setText("");
+ }
+ }
+
+ // finished running
+ _isRunning = false;
+ }
+
+
+ /**
+ * Adjust the column widths and row heights to fit the displayed data
+ */
+ private void adjustTable()
+ {
+ if (!_tableModel.isEmpty())
+ {
+ // adjust column widths for all columns
+ for (int i=0; i<_forecastsTable.getColumnCount(); i++)
+ {
+ double maxWidth = 0.0;
+ for (int j=0; j<_forecastsTable.getRowCount(); j++)
+ {
+ final String value = _tableModel.getValueAt(j, i).toString();
+ maxWidth = Math.max(maxWidth, _forecastsTable.getCellRenderer(0, 0).getTableCellRendererComponent(
+ _forecastsTable, value, false, false, 0, 0).getPreferredSize().getWidth());
+ }
+ _forecastsTable.getColumnModel().getColumn(i).setMinWidth((int) maxWidth + 2);
+ }
+ // Set minimum row heights
+ final int labelHeight = (int) (_forecastsTable.getCellRenderer(0, 0).getTableCellRendererComponent(
+ _forecastsTable, "M", false, false, 0, 0).getMinimumSize().getHeight() * 1.2f + 4);
+ for (int i=0; i<_forecastsTable.getRowCount(); i++)
+ {
+ if (i == WeatherTableModel.ROW_ICON) {
+ _forecastsTable.setRowHeight(i, 55);
+ }
+ else {
+ _forecastsTable.setRowHeight(i, labelHeight);
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the current weather using the lat/long and populate _results
+ * @param inUseCelsius true for celsius, false for fahrenheit
+ * @return weather results
+ */
+ private WeatherResults getCurrentWeather(boolean inUseCelsius)
+ {
+ final Track track = _app.getTrackInfo().getTrack();
+ if (track.getNumPoints() < 1) {return null;}
+ // Get coordinates to lookup
+ double lat = 0.0, lon = 0.0;
+ // See if a point is selected, if so use that
+ DataPoint currPoint = _app.getTrackInfo().getCurrentPoint();
+ if (currPoint != null)
+ {
+ // Use selected point
+ lat = currPoint.getLatitude().getDouble();
+ lon = currPoint.getLongitude().getDouble();
+ }
+ else
+ {
+ lat = track.getLatRange().getMidValue();
+ lon = track.getLonRange().getMidValue();
+ }
+
+ InputStream inStream = null;
+ // Build url either with coordinates or with location id if available
+ final String urlString = "http://api.openweathermap.org/data/2.5/weather?"
+ + (_locationId == null ? ("lat=" + NumberUtils.formatNumberUk(lat, 5) + "&lon=" + NumberUtils.formatNumberUk(lon, 5))
+ : ("id=" + _locationId))
+ + "&lang=" + I18nManager.getText("openweathermap.lang")
+ + "&mode=xml&units=" + (inUseCelsius ? "metric" : "imperial");
+ // System.out.println(urlString);
+
+ // Parse the returned XML with a special handler
+ OWMCurrentHandler xmlHandler = new OWMCurrentHandler();
+ try
+ {
+ URL url = new URL(urlString);
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ // DEBUG: Simulate the call in case of no network connection
+ if (SIMULATE_WITH_FILES)
+ {
+ inStream = new FileInputStream(new File("tim/prune/test/examplecurrentweather.xml"));
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException tie) {}
+ }
+ else
+ {
+ URLConnection conn = url.openConnection();
+ conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
+ inStream = conn.getInputStream();
+ }
+
+ saxParser.parse(inStream, xmlHandler);
+ }
+ catch (Exception e)
+ {
+ // Show error message but don't close dialog
+ _app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
+ _isRunning = false;
+ return null;
+ }
+ // Close stream and ignore errors
+ try {
+ inStream.close();
+ } catch (Exception e) {}
+
+ // Save the location id
+ if (xmlHandler.getLocationId() != null) {
+ _locationId = xmlHandler.getLocationId();
+ }
+ // Get the results from the handler and return
+ WeatherResults results = new WeatherResults();
+ results.setForecast(xmlHandler.getCurrentWeather());
+ results.setLocationName(xmlHandler.getLocationName());
+ results.setUpdateTime(xmlHandler.getUpdateTime());
+ results.setSunriseSunsetTimes(xmlHandler.getSunriseTime(), xmlHandler.getSunsetTime());
+ results.setTempsCelsius(inUseCelsius);
+ return results;
+ }
+
+
+ /**
+ * Get the weather forecast for the current location id and populate in _results
+ * @param inDaily true for daily, false for 3-hourly
+ * @param inCelsius true for celsius, false for fahrenheit
+ * @return weather results
+ */
+ private WeatherResults getWeatherForecast(boolean inDaily, boolean inCelsius)
+ {
+ InputStream inStream = null;
+ // Build URL
+ final String forecastCount = inDaily ? "8" : "3";
+ final String urlString = "http://api.openweathermap.org/data/2.5/forecast"
+ + (inDaily ? "/daily" : "") + "?id=" + _locationId + "&lang=" + I18nManager.getText("openweathermap.lang")
+ + "&mode=xml&units=" + (inCelsius ? "metric" : "imperial") + "&cnt=" + forecastCount;
+ // System.out.println(urlString);
+
+ // Parse the returned XML with a special handler
+ OWMForecastHandler xmlHandler = new OWMForecastHandler();
+ try
+ {
+ URL url = new URL(urlString);
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ // DEBUG: Simulate the call in case of no network connection
+ if (SIMULATE_WITH_FILES)
+ {
+ inStream = new FileInputStream(new File("tim/prune/test/exampleweatherforecast.xml"));
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException tie) {}
+ }
+ else
+ {
+ URLConnection conn = url.openConnection();
+ conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
+ inStream = conn.getInputStream();
+ }
+
+ saxParser.parse(inStream, xmlHandler);
+ }
+ catch (Exception e)
+ {
+ // Show error message but don't close dialog
+ _app.showErrorMessageNoLookup(getNameKey(), e.getClass().getName() + " - " + e.getMessage());
+ _isRunning = false;
+ return null;
+ }
+ // Close stream and ignore errors
+ try {
+ inStream.close();
+ } catch (Exception e) {}
+
+ // Get results from handler, put in model
+ WeatherResults results = new WeatherResults();
+ results.setForecasts(xmlHandler.getForecasts());
+ results.setLocationName(xmlHandler.getLocationName());
+ results.setUpdateTime(xmlHandler.getUpdateTime());
+ results.setTempsCelsius(inCelsius);
+ return results;
+ }
+}