]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - src/tim/prune/function/weather/GetWeatherForecastFunction.java
Moved source into separate src directory due to popular request
[GpsPrune.git] / src / tim / prune / function / weather / GetWeatherForecastFunction.java
diff --git a/src/tim/prune/function/weather/GetWeatherForecastFunction.java b/src/tim/prune/function/weather/GetWeatherForecastFunction.java
new file mode 100644 (file)
index 0000000..970d022
--- /dev/null
@@ -0,0 +1,484 @@
+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;
+       /** Unique API key for GpsPrune */
+       private static final String OPENWEATHERMAP_API_KEY = "d1c5d792362f5a5c2eacf70a3b72ecd6";
+
+
+       /**
+        * 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")
+                       + "&APPID=" + OPENWEATHERMAP_API_KEY;
+               // 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
+                       + "&APPID=" + OPENWEATHERMAP_API_KEY;
+               // 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;
+       }
+}