001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.gui.swing.referencing;
018:
019: // Time
020: import java.util.Date;
021: import java.util.TimeZone;
022: import java.util.Calendar;
023:
024: // Geometry and coordinates
025: import java.awt.Dimension;
026: import java.awt.geom.Dimension2D;
027: import java.awt.geom.Rectangle2D;
028:
029: // User interface (Swing)
030: import java.awt.Insets;
031: import java.awt.Component;
032: import java.awt.BorderLayout;
033: import java.awt.GridBagLayout;
034: import java.awt.GridBagConstraints;
035: import javax.swing.JComponent;
036: import javax.swing.JPanel;
037: import javax.swing.JLabel;
038: import javax.swing.JComboBox;
039: import javax.swing.JTextField;
040: import javax.swing.JOptionPane;
041: import javax.swing.ButtonGroup;
042: import javax.swing.ButtonModel;
043: import javax.swing.JRadioButton;
044: import javax.swing.BorderFactory;
045: import javax.swing.AbstractButton;
046: import javax.swing.JSpinner;
047: import javax.swing.SpinnerModel;
048: import javax.swing.SpinnerDateModel;
049: import javax.swing.SpinnerNumberModel;
050: import javax.swing.AbstractSpinnerModel;
051: import javax.swing.JFormattedTextField;
052: import javax.swing.text.InternationalFormatter;
053:
054: // Events
055: import java.awt.EventQueue;
056: import java.util.EventListener;
057: import java.awt.event.ActionEvent;
058: import java.awt.event.ActionListener;
059: import javax.swing.event.ChangeEvent;
060: import javax.swing.event.ChangeListener;
061:
062: // Parsing and formating
063: import java.text.Format;
064: import java.text.DateFormat;
065: import java.text.NumberFormat;
066: import java.text.ParseException;
067:
068: // Miscellaneous
069: import java.util.Arrays;
070: import java.util.Locale;
071:
072: // Geotools dependencies
073: import org.geotools.measure.Angle;
074: import org.geotools.measure.Latitude;
075: import org.geotools.measure.Longitude;
076: import org.geotools.measure.AngleFormat;
077:
078: // Resources
079: import org.geotools.resources.SwingUtilities;
080: import org.geotools.resources.i18n.Errors;
081: import org.geotools.resources.i18n.ErrorKeys;
082: import org.geotools.resources.i18n.Vocabulary;
083: import org.geotools.resources.i18n.VocabularyKeys;
084: import org.geotools.resources.geometry.XDimension2D;
085:
086: /**
087: * A pane of controls designed to allow a user to select spatio-temporal coordinates.
088: * Current implementation uses geographic coordinates (longitudes/latitudes) and dates
089: * according some locale calendar. Future version may allow the use of user-specified
090: * coordinate system. Latitudes are constrained in the range 90°S to 90°N inclusive.
091: * Longitudes are constrained in the range 180°W to 180°E inclusive. By default, dates
092: * are constrained in the range January 1st, 1970 up to the date at the time the widget
093: * was created.
094: *
095: * <p> </p>
096: * <p align="center"><img src="doc-files/CoordinateChooser.png"></p>
097: * <p> </p>
098: *
099: * @since 2.3
100: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/referencing/CoordinateChooser.java $
101: * @version $Id: CoordinateChooser.java 20883 2006-08-07 13:48:09Z jgarnett $
102: * @author Martin Desruisseaux
103: */
104: public class CoordinateChooser extends JPanel {
105: /**
106: * An enumeration constant for showing or hidding the geographic area selector.
107: * Used as argument for {@link #isSelectorVisible} and {@link #setSelectorVisible}.
108: *
109: * @see #TIME_RANGE
110: * @see #RESOLUTION
111: * @see #isSelectorVisible
112: * @see #setSelectorVisible
113: * @see #addChangeListener
114: * @see #removeChangeListener
115: */
116: public static final int GEOGRAPHIC_AREA = 1;
117:
118: /**
119: * An enumeration constant for showing or hidding the time range selector.
120: * Used as argument for {@link #isSelectorVisible} and {@link #setSelectorVisible}.
121: *
122: * @see #GEOGRAPHIC_AREA
123: * @see #RESOLUTION
124: * @see #isSelectorVisible
125: * @see #setSelectorVisible
126: * @see #addChangeListener
127: * @see #removeChangeListener
128: */
129: public static final int TIME_RANGE = 2;
130:
131: /**
132: * An enumeration constant for showing or hidding the resolution selector.
133: * Used as argument for {@link #isSelectorVisible} and {@link #setSelectorVisible}.
134: *
135: * @see #GEOGRAPHIC_AREA
136: * @see #TIME_RANGE
137: * @see #isSelectorVisible
138: * @see #setSelectorVisible
139: * @see #addChangeListener
140: * @see #removeChangeListener
141: */
142: public static final int RESOLUTION = 4;
143:
144: /**
145: * The three mean panels in this dialog box:
146: * geographic area, time and preferred resolution.
147: */
148: private final JComponent areaPanel, timePanel, resoPanel;
149:
150: /**
151: * Liste de choix dans laquelle l'utilisateur
152: * choisira le fuseau horaire de ses dates.
153: */
154: private final JComboBox timezone;
155:
156: /**
157: * Dates de début et de fin de la plage de temps demandée par l'utilisateur.
158: * Ces dates sont gérées par un modèle {@link SpinnerDateModel}.
159: */
160: private final JSpinner tmin, tmax;
161:
162: /**
163: * Longitudes et latitudes minimales et maximales demandées par l'utilisateur.
164: * Ces coordonnées sont gérées par un modèle {@link SpinnerNumberModel}.
165: */
166: private final JSpinner xmin, xmax, ymin, ymax;
167:
168: /**
169: * Résolution (en minutes de longitudes et de latitudes) demandée par l'utilisateur.
170: * Ces résolution sont gérées par un modèle {@link SpinnerNumberModel}.
171: */
172: private final JSpinner xres, yres;
173:
174: /**
175: * Bouton radio pour sélectioner la meilleure résolution possible.
176: */
177: private final AbstractButton radioBestRes;
178:
179: /**
180: * Bouton radio pour sélectioner la résolution spécifiée.
181: */
182: private final AbstractButton radioPrefRes;
183:
184: /**
185: * Composante facultative à afficher à la droite du paneau {@code CoordinateChooser}.
186: */
187: private JComponent accessory;
188:
189: /**
190: * Class encompassing various listeners for users selections.
191: *
192: * @version $Id: CoordinateChooser.java 20883 2006-08-07 13:48:09Z jgarnett $
193: * @author Martin Desruisseaux
194: */
195: private final class Listeners implements ActionListener,
196: ChangeListener {
197: /**
198: * List of components to toggle.
199: */
200: private final JComponent[] toggle;
201:
202: /**
203: * Constructs a {@code Listeners} object.
204: */
205: public Listeners(final JComponent[] toggle) {
206: this .toggle = toggle;
207: }
208:
209: /**
210: * Invoked when user select a new timezone.
211: */
212: public void actionPerformed(final ActionEvent event) {
213: update(getTimeZone());
214: }
215:
216: /**
217: * Invoked when user change the button radio state
218: * ("use best resolution" / "set resolution").
219: */
220: public void stateChanged(final ChangeEvent event) {
221: setEnabled(radioPrefRes.isSelected());
222: }
223:
224: /**
225: * Enable or disable {@link #toggle} components.
226: */
227: final void setEnabled(final boolean state) {
228: for (int i = 0; i < toggle.length; i++) {
229: toggle[i].setEnabled(state);
230: }
231: }
232: }
233:
234: /**
235: * Constructs a default coordinate chooser. Date will be constrained in the range from
236: * January 1st, 1970 00:00 UTC up to the {@linkplain System#currentTimeMillis current time}.
237: */
238: public CoordinateChooser() {
239: this (new Date(0), new Date());
240: }
241:
242: /**
243: * Constructs a coordinate chooser with date constrained in the specified range.
244: * Note that the {@code [minTime..maxTime]} range is not the same than the
245: * range given to {@link #setTimeRange}. The later set only the time range shown
246: * in the widget, while this constructor set also the minimum and maximum dates
247: * allowed.
248: *
249: * @param minTime The minimal date allowed.
250: * @param maxTime the maximal date allowed.
251: */
252: public CoordinateChooser(final Date minTime, final Date maxTime) {
253: super (new GridBagLayout());
254: final Locale locale = getDefaultLocale();
255: final int timeField = Calendar.DAY_OF_YEAR;
256: final Vocabulary resources = Vocabulary.getResources(locale);
257:
258: radioBestRes = new JRadioButton(resources
259: .getString(VocabularyKeys.USE_BEST_RESOLUTION), true);
260: radioPrefRes = new JRadioButton(resources
261: .getString(VocabularyKeys.SET_PREFERRED_RESOLUTION));
262:
263: tmin = new JSpinner(new SpinnerDateModel(minTime, minTime,
264: maxTime, timeField));
265: tmax = new JSpinner(new SpinnerDateModel(maxTime, minTime,
266: maxTime, timeField));
267: xmin = new JSpinner(new SpinnerAngleModel(new Longitude(
268: Longitude.MIN_VALUE)));
269: xmax = new JSpinner(new SpinnerAngleModel(new Longitude(
270: Longitude.MAX_VALUE)));
271: ymin = new JSpinner(new SpinnerAngleModel(new Latitude(
272: Latitude.MIN_VALUE)));
273: ymax = new JSpinner(new SpinnerAngleModel(new Latitude(
274: Latitude.MAX_VALUE)));
275: xres = new JSpinner(new SpinnerNumberModel(1, 0, 360 * 60, 1));
276: yres = new JSpinner(new SpinnerNumberModel(1, 0, 180 * 60, 1));
277:
278: final AngleFormat angleFormat = new AngleFormat("D°MM.m'",
279: locale);
280: final DateFormat dateFormat = DateFormat.getDateTimeInstance(
281: DateFormat.SHORT, DateFormat.SHORT, locale);
282: final NumberFormat numberFormat = NumberFormat
283: .getNumberInstance(locale);
284: xmin.setEditor(new SpinnerAngleModel.Editor(xmin, angleFormat));
285: xmax.setEditor(new SpinnerAngleModel.Editor(xmax, angleFormat));
286: ymin.setEditor(new SpinnerAngleModel.Editor(ymin, angleFormat));
287: ymax.setEditor(new SpinnerAngleModel.Editor(ymax, angleFormat));
288:
289: setup(tmin, 10, dateFormat);
290: setup(tmax, 10, dateFormat);
291: setup(xmin, 7, null);
292: setup(xmax, 7, null);
293: setup(ymin, 7, null);
294: setup(ymax, 7, null);
295: setup(xres, 3, numberFormat);
296: setup(yres, 3, numberFormat);
297:
298: final String[] timezones = TimeZone.getAvailableIDs();
299: Arrays.sort(timezones);
300: timezone = new JComboBox(timezones);
301: timezone.setSelectedItem(dateFormat.getTimeZone().getID());
302:
303: final JLabel labelSize1 = new JLabel(resources
304: .getLabel(VocabularyKeys.SIZE_IN_MINUTES));
305: final JLabel labelSize2 = new JLabel("\u00D7" /* Multiplication symbol */);
306: final ButtonGroup group = new ButtonGroup();
307: group.add(radioBestRes);
308: group.add(radioPrefRes);
309:
310: final Listeners listeners = new Listeners(new JComponent[] {
311: labelSize1, labelSize2, xres, yres });
312: listeners.setEnabled(false);
313: timezone.addActionListener(listeners);
314: radioPrefRes.addChangeListener(listeners);
315:
316: areaPanel = getPanel(resources
317: .getString(VocabularyKeys.GEOGRAPHIC_COORDINATES));
318: timePanel = getPanel(resources
319: .getString(VocabularyKeys.TIME_RANGE));
320: resoPanel = getPanel(resources
321: .getString(VocabularyKeys.PREFERRED_RESOLUTION));
322: final GridBagConstraints c = new GridBagConstraints();
323:
324: c.weightx = 1;
325: c.gridx = 1;
326: c.gridy = 0;
327: areaPanel.add(ymax, c);
328: c.gridx = 0;
329: c.gridy = 1;
330: areaPanel.add(xmin, c);
331: c.gridx = 2;
332: c.gridy = 1;
333: areaPanel.add(xmax, c);
334: c.gridx = 1;
335: c.gridy = 2;
336: areaPanel.add(ymin, c);
337:
338: JLabel label;
339: c.gridx = 0;
340: c.anchor = c.WEST;
341: c.insets.right = 3;
342: c.weightx = 0;
343: c.gridy = 0;
344: timePanel.add(label = new JLabel(resources
345: .getLabel(VocabularyKeys.START_TIME)), c);
346: label.setLabelFor(tmin);
347: c.gridy = 1;
348: timePanel.add(label = new JLabel(resources
349: .getLabel(VocabularyKeys.END_TIME)), c);
350: label.setLabelFor(tmax);
351: c.gridy = 2;
352: timePanel.add(label = new JLabel(resources
353: .getLabel(VocabularyKeys.TIME_ZONE)), c);
354: label.setLabelFor(timezone);
355: c.gridwidth = 4;
356: c.gridy = 0;
357: resoPanel.add(radioBestRes, c);
358: c.gridy = 1;
359: resoPanel.add(radioPrefRes, c);
360: c.gridy = 2;
361: c.gridwidth = 1;
362: c.anchor = c.EAST;
363: c.insets.right = c.insets.left = 1;
364: c.weightx = 1;
365: c.gridx = 0;
366: resoPanel.add(labelSize1, c);
367: labelSize1.setLabelFor(xres);
368: c.weightx = 0;
369: c.gridx = 1;
370: resoPanel.add(xres, c);
371: c.gridx = 2;
372: resoPanel.add(labelSize2, c);
373: labelSize2.setLabelFor(yres);
374: c.gridx = 3;
375: resoPanel.add(yres, c);
376:
377: c.gridx = 1;
378: c.fill = c.HORIZONTAL;
379: c.insets.right = c.insets.left = 0;
380: c.weightx = 1;
381: c.gridy = 0;
382: timePanel.add(tmin, c);
383: c.gridy = 1;
384: timePanel.add(tmax, c);
385: c.gridy = 2;
386: timePanel.add(timezone, c);
387:
388: c.insets.right = c.insets.left = c.insets.top = c.insets.bottom = 3;
389: c.gridx = 0;
390: c.anchor = c.CENTER;
391: c.fill = c.BOTH;
392: c.weighty = 1;
393: c.gridy = 0;
394: add(areaPanel, c);
395: c.gridy = 1;
396: add(timePanel, c);
397: c.gridy = 2;
398: add(resoPanel, c);
399: }
400:
401: /**
402: * Retourne un panneau avec une bordure titrée.
403: */
404: private static JPanel getPanel(final String title) {
405: final JPanel panel = new JPanel(new GridBagLayout());
406: panel.setBorder(BorderFactory.createCompoundBorder(
407: BorderFactory.createTitledBorder(title), BorderFactory
408: .createEmptyBorder(6, 6, 6, 6)));
409: return panel;
410: }
411:
412: /**
413: * Définit la largeur (en nombre de colonnes) d'un champ.
414: * Eventuellement, cette méthode peut aussi redéfinir le format.
415: */
416: private static void setup(final JSpinner spinner, final int width,
417: final Format format) {
418: final JFormattedTextField field = ((JSpinner.DefaultEditor) spinner
419: .getEditor()).getTextField();
420: field.setMargin(new Insets(/*top*/0, /*left*/6, /*bottom*/0, /*right*/
421: 3));
422: field.setColumns(width);
423: if (format != null) {
424: ((InternationalFormatter) field.getFormatter())
425: .setFormat(format);
426: }
427: }
428:
429: /**
430: * Tells if a selector is currently visible or not. The default {@code CoordinateChooser}
431: * contains three selectors: one for geographic area, one for time range and one for the
432: * preferred resolution.
433: *
434: * @param selector One of the following constants:
435: * {@link #GEOGRAPHIC_AREA},
436: * {@link #TIME_RANGE} or
437: * {@link #RESOLUTION}.
438: * @return {@code true} if the specified selector is visible, or {@code false} otherwise.
439: * @throws IllegalArgumentException if {@code selector} is not legal.
440: */
441: public boolean isSelectorVisible(final int selector) {
442: switch (selector) {
443: case GEOGRAPHIC_AREA:
444: return areaPanel.isVisible();
445: case TIME_RANGE:
446: return timePanel.isVisible();
447: case RESOLUTION:
448: return resoPanel.isVisible();
449: default:
450: throw new IllegalArgumentException();
451: // TODO: provide some error message.
452: }
453: }
454:
455: /**
456: * Set the visible state of one or many selectors.
457: * All selectors are visible by default.
458: *
459: * @param selectors Any bitwise combinaisons of
460: * {@link #GEOGRAPHIC_AREA},
461: * {@link #TIME_RANGE} and/or
462: * {@link #RESOLUTION}.
463: * @param visible {@code true} to show the selectors, or {@code false} to hide them.
464: * @throws IllegalArgumentException if {@code selectors} contains illegal bits.
465: */
466: public void setSelectorVisible(final int selectors,
467: final boolean visible) {
468: ensureValidSelectors(selectors);
469: if ((selectors & GEOGRAPHIC_AREA) != 0)
470: areaPanel.setVisible(visible);
471: if ((selectors & TIME_RANGE) != 0)
472: timePanel.setVisible(visible);
473: if ((selectors & RESOLUTION) != 0)
474: resoPanel.setVisible(visible);
475: }
476:
477: /**
478: * Ensure that the specified bitwise combinaison of selectors is valid.
479: *
480: * @param selectors Any bitwise combinaisons of
481: * {@link #GEOGRAPHIC_AREA},
482: * {@link #TIME_RANGE} and/or
483: * {@link #RESOLUTION}.
484: * @throws IllegalArgumentException if {@code selectors} contains illegal bits.
485: *
486: * @todo Provide a better error message.
487: */
488: private static void ensureValidSelectors(final int selectors)
489: throws IllegalArgumentException {
490: if ((selectors & ~(GEOGRAPHIC_AREA | TIME_RANGE | RESOLUTION)) != 0) {
491: throw new IllegalArgumentException(String
492: .valueOf(selectors));
493: }
494: }
495:
496: /**
497: * Returns the value for the specified number, or NaN if {@code value} is not a number.
498: */
499: private static double doubleValue(final JSpinner spinner) {
500: final Object value = spinner.getValue();
501: return (value instanceof Number) ? ((Number) value)
502: .doubleValue() : Double.NaN;
503: }
504:
505: /**
506: * Returns the value for the specified angle, or NaN if {@code value} is not an angle.
507: */
508: private static double degrees(final JSpinner spinner,
509: final boolean expectLatitude) {
510: final Object value = spinner.getValue();
511: if (value instanceof Angle) {
512: if (expectLatitude ? (value instanceof Longitude)
513: : (value instanceof Latitude)) {
514: return Double.NaN;
515: }
516: return ((Angle) value).degrees();
517: }
518: return Double.NaN;
519: }
520:
521: /**
522: * Gets the geographic area, in latitude and longitude degrees.
523: */
524: public Rectangle2D getGeographicArea() {
525: final double xmin = degrees(this .xmin, false);
526: final double ymin = degrees(this .ymin, true);
527: final double xmax = degrees(this .xmax, false);
528: final double ymax = degrees(this .ymax, true);
529: return new Rectangle2D.Double(Math.min(xmin, xmax), Math.min(
530: ymin, ymax), Math.abs(xmax - xmin), Math.abs(ymax
531: - ymin));
532: }
533:
534: /**
535: * Sets the geographic area, in latitude and longitude degrees.
536: */
537: public void setGeographicArea(final Rectangle2D area) {
538: xmin.setValue(new Longitude(area.getMinX()));
539: xmax.setValue(new Longitude(area.getMaxX()));
540: ymin.setValue(new Latitude(area.getMinY()));
541: ymax.setValue(new Latitude(area.getMaxY()));
542: }
543:
544: /**
545: * Returns the preferred resolution. A {@code null} value means that the
546: * best available resolution should be used.
547: */
548: public Dimension2D getPreferredResolution() {
549: if (radioPrefRes.isSelected()) {
550: return new XDimension2D.Double(doubleValue(xres),
551: doubleValue(yres));
552: }
553: return null;
554: }
555:
556: /**
557: * Sets the preferred resolution. A {@code null} value means that the best
558: * available resolution should be used.
559: */
560: public void setPreferredResolution(final Dimension2D resolution) {
561: if (resolution != null) {
562: xres.setValue(new Double(resolution.getWidth() * 60));
563: yres.setValue(new Double(resolution.getHeight() * 60));
564: radioPrefRes.setSelected(true);
565: } else {
566: radioBestRes.setSelected(true);
567: }
568: }
569:
570: /**
571: * Returns the time zone used for displaying dates.
572: */
573: public TimeZone getTimeZone() {
574: return TimeZone.getTimeZone(timezone.getSelectedItem()
575: .toString());
576: }
577:
578: /**
579: * Sets the time zone. This method change the control's display.
580: * It doesn't change the date values, i.e. it have no effect
581: * on previous or future call to {@link #setTimeRange}.
582: */
583: public void setTimeZone(final TimeZone timezone) {
584: this .timezone.setSelectedItem(timezone.getID());
585: }
586:
587: /**
588: * Updates the time zone in text fields. This method is automatically invoked
589: * by {@link JComboBox} on user's selection. It is also (indirectly) invoked
590: * on {@link #setTimeZone} call.
591: */
592: private void update(final TimeZone timezone) {
593: boolean refresh = true;
594: try {
595: tmin.commitEdit();
596: tmax.commitEdit();
597: } catch (ParseException exception) {
598: refresh = false;
599: }
600: ((JSpinner.DateEditor) tmin.getEditor()).getFormat()
601: .setTimeZone(timezone);
602: ((JSpinner.DateEditor) tmax.getEditor()).getFormat()
603: .setTimeZone(timezone);
604: if (refresh) {
605: // TODO: If a "JSpinner.reformat()" method was available, we would use it here.
606: fireStateChanged((AbstractSpinnerModel) tmin.getModel());
607: fireStateChanged((AbstractSpinnerModel) tmax.getModel());
608: }
609: }
610:
611: /**
612: * Run each {@link ChangeListener#stateChanged()} method for the specified spinner model.
613: */
614: private static void fireStateChanged(
615: final AbstractSpinnerModel model) {
616: final ChangeEvent changeEvent = new ChangeEvent(model);
617: final EventListener[] listeners = model
618: .getListeners(ChangeListener.class);
619: for (int i = listeners.length; --i >= 0;) {
620: ((ChangeListener) listeners[i]).stateChanged(changeEvent);
621: }
622: }
623:
624: /**
625: * Returns the start time, or {@code null} if there is none.
626: */
627: public Date getStartTime() {
628: return (Date) tmin.getValue();
629: }
630:
631: /**
632: * Returns the end time, or {@code null} if there is none.
633: */
634: public Date getEndTime() {
635: return (Date) tmax.getValue();
636: }
637:
638: /**
639: * Sets the time range.
640: *
641: * @param startTime The start time.
642: * @param endTime The end time.
643: *
644: * @see #getStartTime
645: * @see #getEndTime
646: */
647: public void setTimeRange(final Date startTime, final Date endTime) {
648: tmin.setValue(startTime);
649: tmax.setValue(endTime);
650: }
651:
652: /**
653: * Returns the accessory component.
654: *
655: * @return The accessory component, or {@code null} if there is none.
656: */
657: public JComponent getAccessory() {
658: return accessory;
659: }
660:
661: /**
662: * Sets the accessory component. An accessory is often used to show available data.
663: * However, it can be used for anything that the programmer wishes, such as extra
664: * custom coordinate chooser controls.
665: * <p>
666: * <strong>Note:</strong> If there was a previous accessory, you should unregister any
667: * listeners that the accessory might have registered with the coordinate chooser.
668: *
669: * @param accessory The accessory component, or {@code null} to remove any previous accessory.
670: */
671: public void setAccessory(final JComponent accessory) {
672: synchronized (getTreeLock()) {
673: if (this .accessory != null) {
674: remove(this .accessory);
675: }
676: this .accessory = accessory;
677: if (accessory != null) {
678: final GridBagConstraints c = new GridBagConstraints();
679: c.insets.right = c.insets.left = c.insets.top = c.insets.bottom = 3;
680: c.gridx = 1;
681: c.weightx = 1;
682: c.gridwidth = 1;
683: c.gridy = 0;
684: c.weighty = 1;
685: c.gridheight = 3;
686: c.anchor = c.CENTER;
687: c.fill = c.BOTH;
688: add(accessory, c);
689: }
690: validate();
691: }
692: }
693:
694: /**
695: * Check if an angle is of expected type (latitude or longitude).
696: */
697: private void checkAngle(final JSpinner field,
698: final boolean expectLatitude) throws ParseException {
699: final Object angle = field.getValue();
700: if (expectLatitude ? (angle instanceof Longitude)
701: : (angle instanceof Latitude)) {
702: throw new ParseException(Errors.getResources(getLocale())
703: .getString(ErrorKeys.BAD_COORDINATE_$1, angle), 0);
704: }
705: }
706:
707: /**
708: * Commits the currently edited values. If commit fails, focus will be set on the offending
709: * field.
710: *
711: * @throws ParseException If at least one of currently edited value couldn't be commited.
712: */
713: public void commitEdit() throws ParseException {
714: JSpinner focus = null;
715: try {
716: (focus = tmin).commitEdit();
717: (focus = tmax).commitEdit();
718: (focus = xmin).commitEdit();
719: (focus = xmax).commitEdit();
720: (focus = ymin).commitEdit();
721: (focus = ymax).commitEdit();
722: (focus = xres).commitEdit();
723: (focus = yres).commitEdit();
724:
725: checkAngle(focus = xmin, false);
726: checkAngle(focus = xmax, false);
727: checkAngle(focus = ymin, true);
728: checkAngle(focus = ymax, true);
729: } catch (ParseException exception) {
730: focus.requestFocus();
731: throw exception;
732: }
733: }
734:
735: /**
736: * Prend en compte les valeurs des champs édités par l'utilisateur.
737: * Si les entrés ne sont pas valide, affiche un message d'erreur en
738: * utilisant la fenêtre parente {@code owner} spécifiée.
739: *
740: * @param owner Fenêtre dans laquelle faire apparaître d'eventuels messages d'erreur.
741: * @return {@code true} si la prise en compte des paramètres à réussie.
742: */
743: private boolean commitEdit(final Component owner) {
744: try {
745: commitEdit();
746: } catch (ParseException exception) {
747: SwingUtilities.showMessageDialog(owner, exception
748: .getLocalizedMessage(), Errors.getResources(
749: getLocale()).getString(ErrorKeys.BAD_ENTRY),
750: JOptionPane.ERROR_MESSAGE);
751: return false;
752: }
753: return true;
754: }
755:
756: /**
757: * Adds a change listener to the listener list. This change listener will be notify when
758: * a value changed. The change may be in a geographic coordinate field, a date field, a
759: * resolution field, etc. The watched values depend on the {@code selectors} arguments:
760: * {@link #GEOGRAPHIC_AREA} will watches for the bounding box (East, West, North and South
761: * value); {@link #TIME_RANGE} watches for start time and end time; {@link #RESOLUTION}
762: * watches for the resolution along East-West and North-South axis. Bitwise combinaisons
763: * are allowed. For example, <code>GEOGRAPHIC_AREA | TIME_RANGE</code> will register a
764: * listener for both geographic area and time range.
765: * <p>
766: * The source of {@link ChangeEvent}s delivered to {@link ChangeListener}s will be in most
767: * case the {@link SpinnerModel} for the edited field.
768: *
769: * @param selectors Any bitwise combinaisons of
770: * {@link #GEOGRAPHIC_AREA},
771: * {@link #TIME_RANGE} and/or
772: * {@link #RESOLUTION}.
773: * @param listener The listener to add to the specified selectors.
774: * @throws IllegalArgumentException if {@code selectors} contains illegal bits.
775: */
776: public void addChangeListener(final int selectors,
777: final ChangeListener listener) {
778: ensureValidSelectors(selectors);
779: if ((selectors & GEOGRAPHIC_AREA) != 0) {
780: xmin.getModel().addChangeListener(listener);
781: xmax.getModel().addChangeListener(listener);
782: ymin.getModel().addChangeListener(listener);
783: ymax.getModel().addChangeListener(listener);
784: }
785: if ((selectors & TIME_RANGE) != 0) {
786: tmin.getModel().addChangeListener(listener);
787: tmax.getModel().addChangeListener(listener);
788: }
789: if ((selectors & RESOLUTION) != 0) {
790: xres.getModel().addChangeListener(listener);
791: yres.getModel().addChangeListener(listener);
792: radioPrefRes.getModel().addChangeListener(listener);
793: }
794: }
795:
796: /**
797: * Removes a change listener from the listener list.
798: *
799: * @param selectors Any bitwise combinaisons of
800: * {@link #GEOGRAPHIC_AREA},
801: * {@link #TIME_RANGE} and/or
802: * {@link #RESOLUTION}.
803: * @param listener The listener to remove from the specified selectors.
804: * @throws IllegalArgumentException if {@code selectors} contains illegal bits.
805: */
806: public void removeChangeListener(final int selectors,
807: final ChangeListener listener) {
808: ensureValidSelectors(selectors);
809: if ((selectors & GEOGRAPHIC_AREA) != 0) {
810: xmin.getModel().removeChangeListener(listener);
811: xmax.getModel().removeChangeListener(listener);
812: ymin.getModel().removeChangeListener(listener);
813: ymax.getModel().removeChangeListener(listener);
814: }
815: if ((selectors & TIME_RANGE) != 0) {
816: tmin.getModel().removeChangeListener(listener);
817: tmax.getModel().removeChangeListener(listener);
818: }
819: if ((selectors & RESOLUTION) != 0) {
820: xres.getModel().removeChangeListener(listener);
821: yres.getModel().removeChangeListener(listener);
822: radioPrefRes.getModel().removeChangeListener(listener);
823: }
824: }
825:
826: /**
827: * Shows a dialog box requesting input from the user. The dialog box will be
828: * parented to {@code owner}. If {@code owner} is contained into a
829: * {@link javax.swing.JDesktopPane}, the dialog box will appears as an internal
830: * frame. This method can be invoked from any thread (may or may not be the
831: * <cite>Swing</cite> thread).
832: *
833: * @param owner The parent component for the dialog box, or {@code null} if there is no parent.
834: * @return {@code true} if user pressed the "Ok" button, or {@code false} otherwise
835: * (e.g. pressing "Cancel" or closing the dialog box from the title bar).
836: */
837: public boolean showDialog(final Component owner) {
838: return showDialog(owner, Vocabulary.getResources(getLocale())
839: .getString(VocabularyKeys.COORDINATES_SELECTION));
840: }
841:
842: /**
843: * Shows a dialog box requesting input from the user. If {@code owner} is contained into a
844: * {@link javax.swing.JDesktopPane}, the dialog box will appears as an internal frame. This
845: * method can be invoked from any thread (may or may not be the <cite>Swing</cite> thread).
846: *
847: * @param owner The parent component for the dialog box, or {@code null} if there is no parent.
848: * @param title The dialog box title.
849: * @return {@code true} if user pressed the "Ok" button, or {@code false} otherwise
850: * (e.g. pressing "Cancel" or closing the dialog box from the title bar).
851: */
852: public boolean showDialog(final Component owner, final String title) {
853: while (SwingUtilities.showOptionDialog(owner, this , title)) {
854: if (commitEdit(owner)) {
855: return true;
856: }
857: }
858: return false;
859: }
860:
861: /**
862: * Show the dialog box. This method is provided only as an easy
863: * way to test the dialog appearance from the command line.
864: */
865: public static void main(final String[] args) {
866: new CoordinateChooser().showDialog(null);
867: }
868: }
|