X-Git-Url: https://gitweb.fperrin.net/?p=GpsPrune.git;a=blobdiff_plain;f=src%2Ftim%2Fprune%2Ffunction%2Fweather%2FGetWeatherForecastFunction.java;fp=src%2Ftim%2Fprune%2Ffunction%2Fweather%2FGetWeatherForecastFunction.java;h=970d022f67032ce0e45cc9ffce7e5921abb77486;hp=0000000000000000000000000000000000000000;hb=ce6f2161b8596f7018d6a76bff79bc9e571f35fd;hpb=2d8cb72e84d5cc1089ce77baf1e34ea3ea2f8465 diff --git a/src/tim/prune/function/weather/GetWeatherForecastFunction.java b/src/tim/prune/function/weather/GetWeatherForecastFunction.java new file mode 100644 index 0000000..970d022 --- /dev/null +++ b/src/tim/prune/function/weather/GetWeatherForecastFunction.java @@ -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 _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(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; + } +}