Coverage Report - org.geotoolkit.gui.swing.referencing.CoordinateChooser
 
Classes in this File Line Coverage Branch Coverage Complexity
CoordinateChooser
38 %
83/218
3 %
2/59
2,206
CoordinateChooser$Listeners
60 %
6/10
100 %
2/2
2,206
 
 1  
 /*
 2  
  *    Geotoolkit.org - An Open Source Java GIS Toolkit
 3  
  *    http://www.geotoolkit.org
 4  
  *
 5  
  *    (C) 2001-2012, Open Source Geospatial Foundation (OSGeo)
 6  
  *    (C) 2009-2012, Geomatys
 7  
  *
 8  
  *    This library is free software; you can redistribute it and/or
 9  
  *    modify it under the terms of the GNU Lesser General Public
 10  
  *    License as published by the Free Software Foundation;
 11  
  *    version 2.1 of the License.
 12  
  *
 13  
  *    This library is distributed in the hope that it will be useful,
 14  
  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 15  
  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 16  
  *    Lesser General Public License for more details.
 17  
  */
 18  
 package org.geotoolkit.gui.swing.referencing;
 19  
 
 20  
 import java.util.Date;
 21  
 import java.util.Arrays;
 22  
 import java.util.Locale;
 23  
 import java.util.TimeZone;
 24  
 import java.util.Calendar;
 25  
 import java.util.EventListener;
 26  
 
 27  
 import java.awt.Component;
 28  
 import java.awt.GridBagLayout;
 29  
 import java.awt.GridBagConstraints;
 30  
 import java.awt.geom.Dimension2D;
 31  
 import java.awt.geom.Rectangle2D;
 32  
 import java.awt.event.ActionEvent;
 33  
 import java.awt.event.ActionListener;
 34  
 
 35  
 import javax.swing.JComponent;
 36  
 import javax.swing.JPanel;
 37  
 import javax.swing.JLabel;
 38  
 import javax.swing.JComboBox;
 39  
 import javax.swing.ButtonGroup;
 40  
 import javax.swing.JRadioButton;
 41  
 import javax.swing.BorderFactory;
 42  
 import javax.swing.AbstractButton;
 43  
 import javax.swing.JSpinner;
 44  
 import javax.swing.SpinnerModel;
 45  
 import javax.swing.SpinnerDateModel;
 46  
 import javax.swing.SpinnerNumberModel;
 47  
 import javax.swing.AbstractSpinnerModel;
 48  
 import javax.swing.JFormattedTextField;
 49  
 import javax.swing.text.InternationalFormatter;
 50  
 import javax.swing.event.ChangeEvent;
 51  
 import javax.swing.event.ChangeListener;
 52  
 
 53  
 import java.text.Format;
 54  
 import java.text.DateFormat;
 55  
 import java.text.NumberFormat;
 56  
 import java.text.ParseException;
 57  
 
 58  
 import org.geotoolkit.measure.Angle;
 59  
 import org.geotoolkit.measure.Latitude;
 60  
 import org.geotoolkit.measure.Longitude;
 61  
 import org.geotoolkit.measure.AngleFormat;
 62  
 
 63  
 import org.geotoolkit.resources.Errors;
 64  
 import org.geotoolkit.resources.Vocabulary;
 65  
 import org.geotoolkit.internal.swing.SwingUtilities;
 66  
 import org.geotoolkit.display.shape.DoubleDimension2D;
 67  
 import org.geotoolkit.gui.swing.Dialog;
 68  
 
 69  
 
 70  
 /**
 71  
  * A pane of controls designed to allow a user to select spatio-temporal coordinates.
 72  
  * Current implementation uses geographic coordinates (longitudes/latitudes) and dates
 73  
  * according some locale calendar. Future version may allow the use of user-specified
 74  
  * coordinate system. Latitudes are constrained in the range 90°S to 90°N inclusive.
 75  
  * Longitudes are constrained in the range 180°W to 180°E inclusive. By default, dates
 76  
  * are constrained in the range January 1st, 1970 up to the date at the time the widget
 77  
  * was created.
 78  
  *
 79  
  * <table cellspacing="24" cellpadding="12" align="center"><tr valign="top"><td>
 80  
  * <img src="doc-files/CoordinateChooser.png">
 81  
  * </td><td width="500" bgcolor="lightblue">
 82  
  * {@section Demo}
 83  
  * The image on the left side gives an example of this widget appearance.
 84  
  * To try this component in your browser, see the
 85  
  * <a href="http://www.geotoolkit.org/demos/geotk-simples/applet/CoordinateChooser.html">demonstration applet</a>.
 86  
  * </td></tr></table>
 87  
  *
 88  
  * @author Martin Desruisseaux (IRD, Geomatys)
 89  
  * @version 3.14
 90  
  *
 91  
  * @since 2.3
 92  
  * @module
 93  
  */
 94  
 @SuppressWarnings("serial")
 95  0
 public class CoordinateChooser extends JComponent implements Dialog {
 96  
     /**
 97  
      * The factory by which to multiply the resolution. Current value is 60, because
 98  
      * the resolution is displayed in minutes of angle. A future Geotk version may
 99  
      * replace that factor by something inferred from the CRS.
 100  
      */
 101  
     private static final double RESOLUTION_FACTOR = 60;
 102  
 
 103  
     /**
 104  
      * An enumeration constant for showing or hiding the geographic area selector.
 105  
      * Used as argument for {@link #isSelectorVisible} and {@link #setSelectorVisible}.
 106  
      *
 107  
      * @see #TIME_RANGE
 108  
      * @see #RESOLUTION
 109  
      * @see #isSelectorVisible
 110  
      * @see #setSelectorVisible
 111  
      * @see #addChangeListener
 112  
      * @see #removeChangeListener
 113  
      */
 114  
     public static final int GEOGRAPHIC_AREA = 1;
 115  
 
 116  
     /**
 117  
      * An enumeration constant for showing or hiding the time range selector.
 118  
      * Used as argument for {@link #isSelectorVisible} and {@link #setSelectorVisible}.
 119  
      *
 120  
      * @see #GEOGRAPHIC_AREA
 121  
      * @see #RESOLUTION
 122  
      * @see #isSelectorVisible
 123  
      * @see #setSelectorVisible
 124  
      * @see #addChangeListener
 125  
      * @see #removeChangeListener
 126  
      */
 127  
     public static final int TIME_RANGE = 2;
 128  
 
 129  
     /**
 130  
      * An enumeration constant for showing or hiding the resolution selector.
 131  
      * Used as argument for {@link #isSelectorVisible} and {@link #setSelectorVisible}.
 132  
      *
 133  
      * @see #GEOGRAPHIC_AREA
 134  
      * @see #TIME_RANGE
 135  
      * @see #isSelectorVisible
 136  
      * @see #setSelectorVisible
 137  
      * @see #addChangeListener
 138  
      * @see #removeChangeListener
 139  
      */
 140  
     public static final int RESOLUTION = 4;
 141  
 
 142  
     /**
 143  
      * The three mean panels in this dialog box:
 144  
      * geographic area, time and preferred resolution.
 145  
      */
 146  
     private final JComponent areaPanel, timePanel, resoPanel;
 147  
 
 148  
     /**
 149  
      * Liste de choix dans laquelle l'utilisateur
 150  
      * choisira le fuseau horaire de ses dates.
 151  
      */
 152  
     private final JComboBox timezone;
 153  
 
 154  
     /**
 155  
      * Dates de début et de fin de la plage de temps demandée par l'utilisateur.
 156  
      * Ces dates sont gérées par un modèle {@link SpinnerDateModel}.
 157  
      */
 158  
     private final JSpinner tmin, tmax;
 159  
 
 160  
     /**
 161  
      * Longitudes et latitudes minimales et maximales demandées par l'utilisateur.
 162  
      * Ces coordonnées sont gérées par un modèle {@link SpinnerNumberModel}.
 163  
      */
 164  
     private final JSpinner xmin, xmax, ymin, ymax;
 165  
 
 166  
     /**
 167  
      * Résolution (en minutes de longitudes et de latitudes) demandée par l'utilisateur.
 168  
      * Ces résolution sont gérées par un modèle {@link SpinnerNumberModel}.
 169  
      */
 170  
     private final JSpinner xres, yres;
 171  
 
 172  
     /**
 173  
      * Bouton radio pour sélectioner la meilleure résolution possible.
 174  
      */
 175  
     private final AbstractButton radioBestRes;
 176  
 
 177  
     /**
 178  
      * Bouton radio pour sélectioner la résolution spécifiée.
 179  
      */
 180  
     private final AbstractButton radioPrefRes;
 181  
 
 182  
     /**
 183  
      * Composante facultative à afficher à la droite du paneau {@code CoordinateChooser}.
 184  
      */
 185  
     private JComponent accessory;
 186  
 
 187  
     /**
 188  
      * Class encompassing various listeners for users selections.
 189  
      *
 190  
      * @author Martin Desruisseaux (IRD)
 191  
      * @version 3.00
 192  
      *
 193  
      * @since 2.3
 194  
      * @module
 195  
      */
 196  
     private final class Listeners implements ActionListener, ChangeListener {
 197  
         /**
 198  
          * List of components to toggle.
 199  
          */
 200  
         private final JComponent[] toggle;
 201  
 
 202  
         /**
 203  
          * Constructs a {@code Listeners} object.
 204  
          */
 205  1
         public Listeners(final JComponent[] toggle) {
 206  1
             this.toggle=toggle;
 207  1
         }
 208  
 
 209  
         /**
 210  
          * Invoked when user select a new timezone.
 211  
          */
 212  
         @Override
 213  
         public void actionPerformed(final ActionEvent event) {
 214  0
             update(getTimeZone());
 215  0
         }
 216  
 
 217  
         /**
 218  
          * Invoked when user change the button radio state
 219  
          * ("use best resolution" / "set resolution").
 220  
          */
 221  
         @Override
 222  
         public void stateChanged(final ChangeEvent event) {
 223  0
             setEnabled(radioPrefRes.isSelected());
 224  0
         }
 225  
 
 226  
         /**
 227  
          * Enable or disable {@link #toggle} components.
 228  
          */
 229  
         final void setEnabled(final boolean state) {
 230  5
             for (int i=0; i<toggle.length; i++) {
 231  4
                 toggle[i].setEnabled(state);
 232  
             }
 233  1
         }
 234  
     }
 235  
 
 236  
     /**
 237  
      * Constructs a default coordinate chooser. Date will be constrained in the range from
 238  
      * January 1st, 1970 00:00 UTC up to the {@linkplain System#currentTimeMillis current time}.
 239  
      */
 240  
     public CoordinateChooser() {
 241  1
         this(new Date(0), new Date());
 242  1
     }
 243  
 
 244  
     /**
 245  
      * Constructs a coordinate chooser with date constrained in the specified range.
 246  
      * Note that the {@code [minTime..maxTime]} range is not the same than the
 247  
      * range given to {@link #setTimeRange}. The later set only the time range shown
 248  
      * in the widget, while this constructor set also the minimum and maximum dates
 249  
      * allowed.
 250  
      *
 251  
      * @param minTime The minimal date allowed.
 252  
      * @param maxTime the maximal date allowed.
 253  
      */
 254  1
     public CoordinateChooser(final Date minTime, final Date maxTime) {
 255  1
         setLayout(new GridBagLayout());
 256  1
         final Locale locale = getDefaultLocale();
 257  
         final int timeField = Calendar.DAY_OF_YEAR;
 258  1
         final Vocabulary resources = Vocabulary.getResources(locale);
 259  
 
 260  1
         radioBestRes = new JRadioButton(resources.getString(Vocabulary.Keys.USE_BEST_RESOLUTION), true);
 261  1
         radioPrefRes = new JRadioButton(resources.getString(Vocabulary.Keys.SET_PREFERRED_RESOLUTION));
 262  
 
 263  1
         tmin = new JSpinner(new SpinnerDateModel(minTime, minTime, maxTime, timeField));
 264  1
         tmax = new JSpinner(new SpinnerDateModel(maxTime, minTime, maxTime, timeField));
 265  1
         xmin = new JSpinner(new SpinnerAngleModel(new Longitude(Longitude.MIN_VALUE)));
 266  1
         xmax = new JSpinner(new SpinnerAngleModel(new Longitude(Longitude.MAX_VALUE)));
 267  1
         ymin = new JSpinner(new SpinnerAngleModel(new  Latitude( Latitude.MIN_VALUE)));
 268  1
         ymax = new JSpinner(new SpinnerAngleModel(new  Latitude( Latitude.MAX_VALUE)));
 269  1
         xres = new JSpinner(new SpinnerNumberModel(1d, 0d, 360d*60, 1d));
 270  1
         yres = new JSpinner(new SpinnerNumberModel(1d, 0d, 180d*60, 1d));
 271  
 
 272  1
         final AngleFormat   angleFormat = AngleFormat.getInstance(locale);
 273  1
         final DateFormat     dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
 274  1
         final NumberFormat numberFormat = NumberFormat.getNumberInstance(locale);
 275  1
         xmin.setEditor(new SpinnerAngleModel.Editor(xmin, angleFormat));
 276  1
         xmax.setEditor(new SpinnerAngleModel.Editor(xmax, angleFormat));
 277  1
         ymin.setEditor(new SpinnerAngleModel.Editor(ymin, angleFormat));
 278  1
         ymax.setEditor(new SpinnerAngleModel.Editor(ymax, angleFormat));
 279  
 
 280  1
         setup(tmin, 10,   dateFormat);
 281  1
         setup(tmax, 10,   dateFormat);
 282  1
         setup(xmin,  7,         null);
 283  1
         setup(xmax,  7,         null);
 284  1
         setup(ymin,  7,         null);
 285  1
         setup(ymax,  7,         null);
 286  1
         setup(xres,  3, numberFormat);
 287  1
         setup(yres,  3, numberFormat);
 288  
 
 289  1
         final String[] timezones = TimeZone.getAvailableIDs();
 290  1
         Arrays.sort(timezones);
 291  1
         timezone = new JComboBox(timezones);
 292  1
         timezone.setSelectedItem(dateFormat.getTimeZone().getID());
 293  
 
 294  1
         final JLabel labelSize1 = new JLabel(resources.getLabel(Vocabulary.Keys.SIZE_IN_MINUTES));
 295  1
         final JLabel labelSize2 = new JLabel("\u00D7"  /* Multiplication symbol */);
 296  1
         final ButtonGroup group = new ButtonGroup();
 297  1
         group.add(radioBestRes);
 298  1
         group.add(radioPrefRes);
 299  
 
 300  1
         final Listeners listeners = new Listeners(new JComponent[] {
 301  
                                                   labelSize1, labelSize2, xres, yres});
 302  1
         listeners   .setEnabled(false);
 303  1
         timezone    .addActionListener(listeners);
 304  1
         radioPrefRes.addChangeListener(listeners);
 305  
 
 306  1
         areaPanel = getPanel(resources.getString(Vocabulary.Keys.GEOGRAPHIC_COORDINATES));
 307  1
         timePanel = getPanel(resources.getString(Vocabulary.Keys.TIME_RANGE            ));
 308  1
         resoPanel = getPanel(resources.getString(Vocabulary.Keys.PREFERRED_RESOLUTION  ));
 309  1
         final GridBagConstraints c = new GridBagConstraints();
 310  
 
 311  1
         c.weightx=1;
 312  1
         c.gridx=1; c.gridy=0; areaPanel.add(ymax, c);
 313  1
         c.gridx=0; c.gridy=1; areaPanel.add(xmin, c);
 314  1
         c.gridx=2; c.gridy=1; areaPanel.add(xmax, c);
 315  1
         c.gridx=1; c.gridy=2; areaPanel.add(ymin, c);
 316  
 
 317  
         JLabel label;
 318  1
         c.gridx=0; c.anchor=GridBagConstraints.WEST; c.insets.right=3; c.weightx=0;
 319  1
         c.gridy=0; timePanel.add(label=new JLabel(resources.getLabel(Vocabulary.Keys.START_TIME)), c); label.setLabelFor(tmin);
 320  1
         c.gridy=1; timePanel.add(label=new JLabel(resources.getLabel(Vocabulary.Keys.END_TIME  )), c); label.setLabelFor(tmax);
 321  1
         c.gridy=2; timePanel.add(label=new JLabel(resources.getLabel(Vocabulary.Keys.TIME_ZONE )), c); label.setLabelFor(timezone); c.gridwidth=4;
 322  1
         c.gridy=0; resoPanel.add(radioBestRes,  c);
 323  1
         c.gridy=1; resoPanel.add(radioPrefRes,  c);
 324  1
         c.gridy=2; c.gridwidth=1; c.anchor=GridBagConstraints.EAST; c.insets.right=c.insets.left=1; c.weightx=1;
 325  1
         c.gridx=0; resoPanel.add(labelSize1, c); labelSize1.setLabelFor(xres);  c.weightx=0;
 326  1
         c.gridx=1; resoPanel.add(xres,       c);
 327  1
         c.gridx=2; resoPanel.add(labelSize2, c); labelSize2.setLabelFor(yres);
 328  1
         c.gridx=3; resoPanel.add(yres,       c);
 329  
 
 330  1
         c.gridx=1; c.fill=GridBagConstraints.HORIZONTAL; c.insets.right=c.insets.left=0; c.weightx=1;
 331  1
         c.gridy=0; timePanel.add(tmin,     c);
 332  1
         c.gridy=1; timePanel.add(tmax,     c);
 333  1
         c.gridy=2; timePanel.add(timezone, c);
 334  
 
 335  1
         c.insets.right=c.insets.left=c.insets.top=c.insets.bottom=3;
 336  1
         c.gridx=0; c.anchor=GridBagConstraints.CENTER; c.fill=GridBagConstraints.BOTH; c.weighty=1;
 337  1
         c.gridy=0; add(areaPanel, c);
 338  1
         c.gridy=1; add(timePanel, c);
 339  1
         c.gridy=2; add(resoPanel, c);
 340  1
     }
 341  
 
 342  
     /**
 343  
      * Creates a panel with a titled border.
 344  
      */
 345  
     private static JPanel getPanel(final String title) {
 346  3
         final JPanel panel = new JPanel(new GridBagLayout());
 347  3
         panel.setBorder(BorderFactory.createCompoundBorder(
 348  
                         BorderFactory.createTitledBorder(title),
 349  
                         BorderFactory.createEmptyBorder(6,6,6,6)));
 350  3
         panel.setOpaque(false);
 351  3
         return panel;
 352  
     }
 353  
 
 354  
     /**
 355  
      * Sets the width of the given field, in amount of columns.
 356  
      * As a side effect, this method set also the format.
 357  
      */
 358  
     private static void setup(final JSpinner spinner, final int width, final Format format) {
 359  8
         final JFormattedTextField field = ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField();
 360  8
         field.setColumns(width);
 361  8
         if (format != null) {
 362  4
             ((InternationalFormatter)field.getFormatter()).setFormat(format);
 363  
         }
 364  8
     }
 365  
 
 366  
     /**
 367  
      * Tells if a selector is currently visible or not. The default {@code CoordinateChooser}
 368  
      * contains three selectors: one for geographic area, one for time range and one for the
 369  
      * preferred resolution.
 370  
      *
 371  
      * @param selector One of the following constants:
 372  
      *                 {@link #GEOGRAPHIC_AREA},
 373  
      *                 {@link #TIME_RANGE} or
 374  
      *                 {@link #RESOLUTION}.
 375  
      * @return {@code true} if the specified selector is visible, or {@code false} otherwise.
 376  
      * @throws IllegalArgumentException if {@code selector} is not legal.
 377  
      */
 378  
     public boolean isSelectorVisible(final int selector) {
 379  0
         switch (selector) {
 380  0
             case GEOGRAPHIC_AREA: return areaPanel.isVisible();
 381  0
             case TIME_RANGE:      return timePanel.isVisible();
 382  0
             case RESOLUTION:      return resoPanel.isVisible();
 383  0
             default: throw new IllegalArgumentException();
 384  
                      // TODO: provide some error message.
 385  
         }
 386  
     }
 387  
 
 388  
     /**
 389  
      * Sets the visible state of one or many selectors.
 390  
      * All selectors are visible by default.
 391  
      *
 392  
      * @param  selectors Any bitwise combinations of {@link #GEOGRAPHIC_AREA},
 393  
      *         {@link #TIME_RANGE} and/or {@link #RESOLUTION}.
 394  
      * @param  visible {@code true} to show the selectors, or {@code false} to hide them.
 395  
      * @throws IllegalArgumentException if {@code selectors} contains illegal bits.
 396  
      */
 397  
     public void setSelectorVisible(final int selectors, final boolean visible) {
 398  0
         ensureValidSelectors(selectors);
 399  0
         if ((selectors & GEOGRAPHIC_AREA) != 0) areaPanel.setVisible(visible);
 400  0
         if ((selectors & TIME_RANGE     ) != 0) timePanel.setVisible(visible);
 401  0
         if ((selectors & RESOLUTION     ) != 0) resoPanel.setVisible(visible);
 402  0
     }
 403  
 
 404  
     /**
 405  
      * Ensures that the specified bitwise combination of selectors is valid.
 406  
      *
 407  
      * @param  selectors Any bitwise combinations of {@link #GEOGRAPHIC_AREA},
 408  
      *         {@link #TIME_RANGE} and/or {@link #RESOLUTION}.
 409  
      * @throws IllegalArgumentException if {@code selectors} contains illegal bits.
 410  
      *
 411  
      * @todo Provide a better error message.
 412  
      */
 413  
     private static void ensureValidSelectors(final int selectors) throws IllegalArgumentException {
 414  0
         if ((selectors & ~(GEOGRAPHIC_AREA | TIME_RANGE | RESOLUTION)) != 0) {
 415  0
             throw new IllegalArgumentException(String.valueOf(selectors));
 416  
         }
 417  0
     }
 418  
 
 419  
     /**
 420  
      * Same as {@link Math#min(double, double}, except that the {@code ceil}
 421  
      * argument is returned if the {@code value} argument is {@code NaN}.
 422  
      */
 423  
     private static double min(final double ceil, final double value) {
 424  0
         return (value < ceil) ? value : ceil;
 425  
     }
 426  
 
 427  
     /**
 428  
      * Same as {@link Math#max(double, double}, except that the {@code floor}
 429  
      * argument is returned if the {@code value} argument is {@code NaN}.
 430  
      */
 431  
     private static double max(final double floor, final double value) {
 432  0
         return (value > floor) ? value : floor;
 433  
     }
 434  
 
 435  
     /**
 436  
      * Returns the value for the specified number, or NaN if {@code value} is not a number.
 437  
      */
 438  
     private static double doubleValue(final JSpinner spinner) {
 439  0
         final Object value = spinner.getValue();
 440  0
         return (value instanceof Number) ? ((Number) value).doubleValue() : Double.NaN;
 441  
     }
 442  
 
 443  
     /**
 444  
      * Returns the value for the specified angle, or NaN if {@code value} is not an angle.
 445  
      */
 446  
     private static double degrees(final JSpinner spinner, final boolean expectLatitude) {
 447  0
         final Object value = spinner.getValue();
 448  0
         if (value instanceof Angle) {
 449  0
             if (expectLatitude ? (value instanceof Longitude) : (value instanceof Latitude)) {
 450  0
                 return Double.NaN;
 451  
             }
 452  0
             return ((Angle) value).degrees();
 453  
         }
 454  0
         return Double.NaN;
 455  
     }
 456  
 
 457  
     /**
 458  
      * Gets the geographic area, in latitude and longitude degrees.
 459  
      *
 460  
      * @return The current geographic area of interest.
 461  
      */
 462  
     public Rectangle2D getGeographicArea() {
 463  0
         final double xmin = degrees(this.xmin, false);
 464  0
         final double ymin = degrees(this.ymin,  true);
 465  0
         final double xmax = degrees(this.xmax, false);
 466  0
         final double ymax = degrees(this.ymax,  true);
 467  0
         return new Rectangle2D.Double(
 468  
                 Math.min(xmin,  xmax), Math.min(ymin,  ymax),
 469  
                 Math.abs(xmax - xmin), Math.abs(ymax - ymin));
 470  
     }
 471  
 
 472  
     /**
 473  
      * Sets the geographic area, in latitude and longitude degrees.
 474  
      *
 475  
      * @param area The new geographic area of interest.
 476  
      */
 477  
     public void setGeographicArea(final Rectangle2D area) {
 478  
         // We allow [-360…360]° range, since the [0…360]° range is sometime used.
 479  0
         xmin.setValue(new Longitude(max(2*Longitude.MIN_VALUE, area.getMinX())));
 480  0
         xmax.setValue(new Longitude(min(2*Longitude.MAX_VALUE, area.getMaxX())));
 481  0
         ymin.setValue(new  Latitude(max(   Latitude.MIN_VALUE, area.getMinY())));
 482  0
         ymax.setValue(new  Latitude(min(   Latitude.MAX_VALUE, area.getMaxY())));
 483  0
     }
 484  
 
 485  
     /**
 486  
      * Returns the preferred resolution. A {@code null} value means that the
 487  
      * best available resolution should be used.
 488  
      *
 489  
      * @return The current preferred resolution, or {@code null} for the best available one.
 490  
      */
 491  
     public Dimension2D getPreferredResolution() {
 492  0
         if (radioPrefRes.isSelected()) {
 493  0
             return new DoubleDimension2D(
 494  
                     doubleValue(xres) / RESOLUTION_FACTOR,
 495  
                     doubleValue(yres) / RESOLUTION_FACTOR);
 496  
         }
 497  0
         return null;
 498  
     }
 499  
 
 500  
     /**
 501  
      * Sets the preferred resolution. A {@code null} value means that the best
 502  
      * available resolution should be used.
 503  
      *
 504  
      * @param resolution The new preferred resolution, or {@code null} for the best available one.
 505  
      */
 506  
     public void setPreferredResolution(final Dimension2D resolution) {
 507  0
         if (resolution != null) {
 508  0
             xres.setValue(Double.valueOf(max(0, resolution.getWidth () * RESOLUTION_FACTOR)));
 509  0
             yres.setValue(Double.valueOf(max(0, resolution.getHeight() * RESOLUTION_FACTOR)));
 510  0
             radioPrefRes.setSelected(true);
 511  
         }  else {
 512  0
             radioBestRes.setSelected(true);
 513  
         }
 514  0
     }
 515  
 
 516  
     /**
 517  
      * Returns the time zone used for displaying dates.
 518  
      *
 519  
      * @return The current timezone.
 520  
      */
 521  
     public TimeZone getTimeZone() {
 522  0
         return TimeZone.getTimeZone(timezone.getSelectedItem().toString());
 523  
     }
 524  
 
 525  
     /**
 526  
      * Sets the time zone. This method change the control's display. It doesn't change the
 527  
      * date values, i.e. it has no effect on previous or future call to {@link #setTimeRange}.
 528  
      *
 529  
      * @param timezone The new timezone.
 530  
      */
 531  
     public void setTimeZone(final TimeZone timezone) {
 532  0
         this.timezone.setSelectedItem(timezone.getID());
 533  0
     }
 534  
 
 535  
     /**
 536  
      * Updates the time zone in text fields. This method is automatically invoked
 537  
      * by {@link JComboBox} on user's selection. It is also (indirectly) invoked
 538  
      * on {@link #setTimeZone} call.
 539  
      */
 540  
     private void update(final TimeZone timezone) {
 541  0
         boolean refresh=true;
 542  
         try {
 543  0
             tmin.commitEdit();
 544  0
             tmax.commitEdit();
 545  0
         } catch (ParseException exception) {
 546  0
             refresh = false;
 547  0
         }
 548  0
         ((JSpinner.DateEditor)tmin.getEditor()).getFormat().setTimeZone(timezone);
 549  0
         ((JSpinner.DateEditor)tmax.getEditor()).getFormat().setTimeZone(timezone);
 550  0
         if (refresh) {
 551  
             // TODO: If a "JSpinner.reformat()" method was available, we would use it here.
 552  0
             fireStateChanged((AbstractSpinnerModel)tmin.getModel());
 553  0
             fireStateChanged((AbstractSpinnerModel)tmax.getModel());
 554  
         }
 555  0
     }
 556  
 
 557  
     /**
 558  
      * Run each {@link ChangeListener#stateChanged()} method for the specified spinner model.
 559  
      */
 560  
     private static void fireStateChanged(final AbstractSpinnerModel model) {
 561  0
         final ChangeEvent   changeEvent = new ChangeEvent(model);
 562  0
         final EventListener[] listeners = model.getListeners(ChangeListener.class);
 563  0
         for (int i=listeners.length; --i>=0;) {
 564  0
             ((ChangeListener)listeners[i]).stateChanged(changeEvent);
 565  
         }
 566  0
     }
 567  
 
 568  
     /**
 569  
      * Returns the start time, or {@code null} if there is none.
 570  
      *
 571  
      * @return The start time, or {@code null} if none.
 572  
      */
 573  
     public Date getStartTime() {
 574  0
         return (Date) tmin.getValue();
 575  
     }
 576  
 
 577  
     /**
 578  
      * Returns the end time, or {@code null} if there is none.
 579  
      *
 580  
      * @return The end time, or {@code null} if none.
 581  
      */
 582  
     public Date getEndTime() {
 583  0
         return (Date) tmax.getValue();
 584  
     }
 585  
 
 586  
     /**
 587  
      * Sets the time range.
 588  
      *
 589  
      * @param startTime The start time.
 590  
      * @param endTime   The end time.
 591  
      *
 592  
      * @see #getStartTime
 593  
      * @see #getEndTime
 594  
      */
 595  
     public void setTimeRange(final Date startTime, final Date endTime) {
 596  0
         tmin.setValue(startTime);
 597  0
         tmax.setValue(  endTime);
 598  0
     }
 599  
 
 600  
     /**
 601  
      * Returns the accessory component.
 602  
      *
 603  
      * @return The accessory component, or {@code null} if there is none.
 604  
      */
 605  
     public JComponent getAccessory() {
 606  0
         return accessory;
 607  
     }
 608  
 
 609  
     /**
 610  
      * Sets the accessory component. An accessory is often used to show available data.
 611  
      * However, it can be used for anything that the programmer wishes, such as extra
 612  
      * custom coordinate chooser controls.
 613  
      * <p>
 614  
      * <strong>Note:</strong> If there was a previous accessory, you should unregister any
 615  
      * listeners that the accessory might have registered with the coordinate chooser.
 616  
      *
 617  
      * @param accessory The accessory component, or {@code null} to remove any previous accessory.
 618  
      */
 619  
     public void setAccessory(final JComponent accessory) {
 620  0
         synchronized (getTreeLock()) {
 621  0
             if (this.accessory!=null) {
 622  0
                 remove(this.accessory);
 623  
             }
 624  0
             this.accessory = accessory;
 625  0
             if (accessory != null) {
 626  0
                 final GridBagConstraints c = new GridBagConstraints();
 627  0
                 c.insets.right=c.insets.left=c.insets.top=c.insets.bottom=3;
 628  0
                 c.gridx=1; c.weightx=1; c.gridwidth=1;
 629  0
                 c.gridy=0; c.weighty=1; c.gridheight=3;
 630  0
                 c.anchor=GridBagConstraints.CENTER; c.fill=GridBagConstraints.BOTH;
 631  0
                 add(accessory, c);
 632  
             }
 633  0
             validate();
 634  0
         }
 635  0
     }
 636  
 
 637  
     /**
 638  
      * Check if an angle is of expected type (latitude or longitude).
 639  
      */
 640  
     private void checkAngle(final JSpinner field, final boolean expectLatitude) throws ParseException {
 641  0
         final Object angle=field.getValue();
 642  0
         if (expectLatitude ? (angle instanceof Longitude) : (angle instanceof Latitude)) {
 643  0
             throw new ParseException(Errors.getResources(getLocale()).getString(
 644  
                     Errors.Keys.ILLEGAL_COORDINATE_$1, angle), 0);
 645  
         }
 646  0
     }
 647  
 
 648  
     /**
 649  
      * {@inheritDoc}
 650  
      */
 651  
     @Override
 652  
     public void commitEdit() throws ParseException {
 653  0
         JSpinner focus = null;
 654  
         try {
 655  0
             (focus = tmin).commitEdit();
 656  0
             (focus = tmax).commitEdit();
 657  0
             (focus = xmin).commitEdit();
 658  0
             (focus = xmax).commitEdit();
 659  0
             (focus = ymin).commitEdit();
 660  0
             (focus = ymax).commitEdit();
 661  0
             (focus = xres).commitEdit();
 662  0
             (focus = yres).commitEdit();
 663  
 
 664  0
             checkAngle(focus = xmin, false);
 665  0
             checkAngle(focus = xmax, false);
 666  0
             checkAngle(focus = ymin,  true);
 667  0
             checkAngle(focus = ymax,  true);
 668  0
         } catch (ParseException exception) {
 669  0
             focus.requestFocus();
 670  0
             throw exception;
 671  0
         }
 672  0
     }
 673  
 
 674  
     /**
 675  
      * Adds a change listener to the listener list. This change listener will be notify when
 676  
      * a value changed. The change may be in a geographic coordinate field, a date field, a
 677  
      * resolution field, etc. The watched values depend on the {@code selectors} arguments:
 678  
      * {@link #GEOGRAPHIC_AREA} will watches for the bounding box (East, West, North and South
 679  
      * value); {@link #TIME_RANGE} watches for start time and end time; {@link #RESOLUTION}
 680  
      * watches for the resolution along East-West and North-South axis. Bitwise combinations
 681  
      * are allowed. For example, <code>GEOGRAPHIC_AREA | TIME_RANGE</code> will register a
 682  
      * listener for both geographic area and time range.
 683  
      * <p>
 684  
      * The source of {@link ChangeEvent}s delivered to {@link ChangeListener}s will be in most
 685  
      * case the {@link SpinnerModel} for the edited field.
 686  
      *
 687  
      * @param  selectors Any bitwise combinations of
 688  
      *                   {@link #GEOGRAPHIC_AREA},
 689  
      *                   {@link #TIME_RANGE} and/or
 690  
      *                   {@link #RESOLUTION}.
 691  
      * @param  listener The listener to add to the specified selectors.
 692  
      * @throws IllegalArgumentException if {@code selectors} contains illegal bits.
 693  
      */
 694  
     public void addChangeListener(final int selectors, final ChangeListener listener) {
 695  0
         ensureValidSelectors(selectors);
 696  0
         if ((selectors & GEOGRAPHIC_AREA) != 0) {
 697  0
             xmin.getModel().addChangeListener(listener);
 698  0
             xmax.getModel().addChangeListener(listener);
 699  0
             ymin.getModel().addChangeListener(listener);
 700  0
             ymax.getModel().addChangeListener(listener);
 701  
         }
 702  0
         if ((selectors & TIME_RANGE) != 0) {
 703  0
             tmin.getModel().addChangeListener(listener);
 704  0
             tmax.getModel().addChangeListener(listener);
 705  
         }
 706  0
         if ((selectors & RESOLUTION) != 0) {
 707  0
             xres.getModel().addChangeListener(listener);
 708  0
             yres.getModel().addChangeListener(listener);
 709  0
             radioPrefRes.getModel().addChangeListener(listener);
 710  
         }
 711  0
     }
 712  
 
 713  
     /**
 714  
      * Removes a change listener from the listener list.
 715  
      *
 716  
      * @param  selectors Any bitwise combinations of
 717  
      *                   {@link #GEOGRAPHIC_AREA},
 718  
      *                   {@link #TIME_RANGE} and/or
 719  
      *                   {@link #RESOLUTION}.
 720  
      * @param  listener The listener to remove from the specified selectors.
 721  
      * @throws IllegalArgumentException if {@code selectors} contains illegal bits.
 722  
      */
 723  
     public void removeChangeListener(final int selectors, final ChangeListener listener) {
 724  0
         ensureValidSelectors(selectors);
 725  0
         if ((selectors & GEOGRAPHIC_AREA) != 0) {
 726  0
             xmin.getModel().removeChangeListener(listener);
 727  0
             xmax.getModel().removeChangeListener(listener);
 728  0
             ymin.getModel().removeChangeListener(listener);
 729  0
             ymax.getModel().removeChangeListener(listener);
 730  
         }
 731  0
         if ((selectors & TIME_RANGE) != 0) {
 732  0
             tmin.getModel().removeChangeListener(listener);
 733  0
             tmax.getModel().removeChangeListener(listener);
 734  
         }
 735  0
         if ((selectors & RESOLUTION) != 0) {
 736  0
             xres.getModel().removeChangeListener(listener);
 737  0
             yres.getModel().removeChangeListener(listener);
 738  0
             radioPrefRes.getModel().removeChangeListener(listener);
 739  
         }
 740  0
     }
 741  
 
 742  
     /**
 743  
      * Shows a dialog box requesting input from the user. The dialog box will be parented to
 744  
      * {@code owner}. If {@code owner} is contained into a {@link javax.swing.JDesktopPane},
 745  
      * the dialog box will appears as an internal frame.
 746  
      * <p>
 747  
      * This method can be invoked from any thread (may or may not be the <cite>Swing</cite> thread).
 748  
      *
 749  
      * @param  owner The parent component for the dialog box, or {@code null} if there is no parent.
 750  
      * @return {@code true} if user pressed the "Ok" button, or {@code false} otherwise
 751  
      *         (e.g. pressing "Cancel" or closing the dialog box from the title bar).
 752  
      */
 753  
     public boolean showDialog(final Component owner) {
 754  0
         return showDialog(owner, Vocabulary.getResources(getLocale()).
 755  
                 getString(Vocabulary.Keys.COORDINATES_SELECTION));
 756  
     }
 757  
 
 758  
     /**
 759  
      * {@inheritDoc}
 760  
      */
 761  
     @Override
 762  
     public boolean showDialog(final Component owner, final String title) {
 763  0
         return SwingUtilities.showDialog(owner, this, title);
 764  
     }
 765  
 }