001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.widgets;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Dimension;
007import java.awt.GridBagConstraints;
008import java.awt.GridBagLayout;
009import java.awt.Insets;
010
011import javax.swing.BorderFactory;
012import javax.swing.JLabel;
013import javax.swing.JPanel;
014import javax.swing.event.DocumentEvent;
015import javax.swing.event.DocumentListener;
016import javax.swing.text.JTextComponent;
017
018import org.openstreetmap.josm.data.Bounds;
019import org.openstreetmap.josm.data.coor.LatLon;
020import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
021import org.openstreetmap.josm.tools.GBC;
022import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider;
023import org.openstreetmap.josm.tools.Logging;
024import org.openstreetmap.josm.tools.OsmUrlToBounds;
025
026/**
027 * A panel that allows the user to input the coordinates of a lat/lon box
028 */
029public class BoundingBoxSelectionPanel extends JPanel {
030
031    private JosmTextField[] tfLatLon;
032    private final JosmTextField tfOsmUrl = new JosmTextField();
033
034    protected void buildInputFields() {
035        tfLatLon = new JosmTextField[4];
036        for (int i = 0; i < 4; i++) {
037            tfLatLon[i] = new JosmTextField(11);
038            tfLatLon[i].setMinimumSize(new Dimension(100, new JosmTextField().getMinimumSize().height));
039            SelectAllOnFocusGainedDecorator.decorate(tfLatLon[i]);
040        }
041        LatitudeValidator.decorate(tfLatLon[0]);
042        LatitudeValidator.decorate(tfLatLon[2]);
043        LongitudeValidator.decorate(tfLatLon[1]);
044        LongitudeValidator.decorate(tfLatLon[3]);
045    }
046
047    protected final void build() {
048        buildInputFields();
049        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
050        setLayout(new GridBagLayout());
051        tfOsmUrl.getDocument().addDocumentListener(new OsmUrlRefresher());
052
053        // select content on receiving focus. this seems to be the default in the
054        // windows look+feel but not for others. needs invokeLater to avoid strange
055        // side effects that will cancel out the newly made selection otherwise.
056        tfOsmUrl.addFocusListener(new SelectAllOnFocusGainedDecorator());
057
058        add(new JLabel(tr("Min. latitude")), GBC.std().insets(0, 0, 3, 5));
059        add(tfLatLon[0], GBC.std().insets(0, 0, 3, 5));
060        add(new JLabel(tr("Min. longitude")), GBC.std().insets(0, 0, 3, 5));
061        add(tfLatLon[1], GBC.eol());
062        add(new JLabel(tr("Max. latitude")), GBC.std().insets(0, 0, 3, 5));
063        add(tfLatLon[2], GBC.std().insets(0, 0, 3, 5));
064        add(new JLabel(tr("Max. longitude")), GBC.std().insets(0, 0, 3, 5));
065        add(tfLatLon[3], GBC.eol());
066
067        GridBagConstraints gc = new GridBagConstraints();
068        gc.gridx = 0;
069        gc.gridy = 2;
070        gc.gridwidth = 4;
071        gc.fill = GridBagConstraints.HORIZONTAL;
072        gc.weightx = 1.0;
073        gc.insets = new Insets(10, 0, 0, 3);
074        add(new JMultilineLabel(tr("URL from www.openstreetmap.org (you can paste a download URL here to specify a bounding box)")), gc);
075
076        gc.gridy = 3;
077        gc.insets = new Insets(3, 0, 0, 3);
078        add(tfOsmUrl, gc);
079    }
080
081    /**
082     * Constructs a new {@code BoundingBoxSelectionPanel}.
083     */
084    public BoundingBoxSelectionPanel() {
085        build();
086    }
087
088    /**
089     * Sets the bounding box to the given area
090     * @param area The new input values
091     */
092    public void setBoundingBox(Bounds area) {
093        updateBboxFields(area);
094    }
095
096    /**
097     * Get the bounding box the user selected
098     * @return The box or <code>null</code> if no valid data was input.
099     */
100    public Bounds getBoundingBox() {
101        double minlon, minlat, maxlon, maxlat;
102        try {
103            minlat = JosmDecimalFormatSymbolsProvider.parseDouble(tfLatLon[0].getText().trim());
104            minlon = JosmDecimalFormatSymbolsProvider.parseDouble(tfLatLon[1].getText().trim());
105            maxlat = JosmDecimalFormatSymbolsProvider.parseDouble(tfLatLon[2].getText().trim());
106            maxlon = JosmDecimalFormatSymbolsProvider.parseDouble(tfLatLon[3].getText().trim());
107        } catch (NumberFormatException e) {
108            Logging.trace(e);
109            return null;
110        }
111        if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)
112         || !LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat))
113            return null;
114        if (minlon > maxlon)
115            return null;
116        if (minlat > maxlat)
117            return null;
118        return new Bounds(minlon, minlat, maxlon, maxlat);
119    }
120
121    private boolean parseURL() {
122        Bounds b = OsmUrlToBounds.parse(tfOsmUrl.getText());
123        if (b == null) return false;
124        updateBboxFields(b);
125        return true;
126    }
127
128    private void updateBboxFields(Bounds area) {
129        if (area == null) return;
130        tfLatLon[0].setText(DecimalDegreesCoordinateFormat.INSTANCE.latToString(area.getMin()));
131        tfLatLon[1].setText(DecimalDegreesCoordinateFormat.INSTANCE.lonToString(area.getMin()));
132        tfLatLon[2].setText(DecimalDegreesCoordinateFormat.INSTANCE.latToString(area.getMax()));
133        tfLatLon[3].setText(DecimalDegreesCoordinateFormat.INSTANCE.lonToString(area.getMax()));
134    }
135
136    private static class LatitudeValidator extends AbstractTextComponentValidator {
137
138        public static void decorate(JTextComponent tc) {
139            new LatitudeValidator(tc);
140        }
141
142        LatitudeValidator(JTextComponent tc) {
143            super(tc);
144        }
145
146        @Override
147        public void validate() {
148            double value = 0;
149            try {
150                value = JosmDecimalFormatSymbolsProvider.parseDouble(getComponent().getText());
151            } catch (NumberFormatException ex) {
152                feedbackInvalid(tr("The string ''{0}'' is not a valid double value.", getComponent().getText()));
153                Logging.trace(ex);
154                return;
155            }
156            if (!LatLon.isValidLat(value)) {
157                feedbackInvalid(tr("Value for latitude in range [-90,90] required.", getComponent().getText()));
158                return;
159            }
160            feedbackValid("");
161        }
162
163        @Override
164        public boolean isValid() {
165            try {
166                return LatLon.isValidLat(JosmDecimalFormatSymbolsProvider.parseDouble(getComponent().getText()));
167            } catch (NumberFormatException ex) {
168                Logging.trace(ex);
169                return false;
170            }
171        }
172    }
173
174    private static class LongitudeValidator extends AbstractTextComponentValidator {
175
176        public static void decorate(JTextComponent tc) {
177            new LongitudeValidator(tc);
178        }
179
180        LongitudeValidator(JTextComponent tc) {
181            super(tc);
182        }
183
184        @Override
185        public void validate() {
186            double value = 0;
187            try {
188                value = JosmDecimalFormatSymbolsProvider.parseDouble(getComponent().getText());
189            } catch (NumberFormatException ex) {
190                feedbackInvalid(tr("The string ''{0}'' is not a valid double value.", getComponent().getText()));
191                Logging.trace(ex);
192                return;
193            }
194            if (!LatLon.isValidLon(value)) {
195                feedbackInvalid(tr("Value for longitude in range [-180,180] required.", getComponent().getText()));
196                return;
197            }
198            feedbackValid("");
199        }
200
201        @Override
202        public boolean isValid() {
203            try {
204                return LatLon.isValidLon(JosmDecimalFormatSymbolsProvider.parseDouble(getComponent().getText()));
205            } catch (NumberFormatException ex) {
206                Logging.trace(ex);
207                return false;
208            }
209        }
210    }
211
212    class OsmUrlRefresher implements DocumentListener {
213        @Override
214        public void changedUpdate(DocumentEvent e) {
215            parseURL();
216        }
217
218        @Override
219        public void insertUpdate(DocumentEvent e) {
220            parseURL();
221        }
222
223        @Override
224        public void removeUpdate(DocumentEvent e) {
225            parseURL();
226        }
227    }
228}