001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.download;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Dimension;
008import java.awt.GridBagLayout;
009import java.awt.event.ActionEvent;
010import java.awt.event.ActionListener;
011import java.awt.event.FocusAdapter;
012import java.awt.event.FocusEvent;
013import java.awt.event.MouseAdapter;
014import java.awt.event.MouseEvent;
015
016import javax.swing.BorderFactory;
017import javax.swing.JButton;
018import javax.swing.JLabel;
019import javax.swing.JPanel;
020import javax.swing.UIManager;
021import javax.swing.border.Border;
022import javax.swing.event.DocumentEvent;
023import javax.swing.event.DocumentListener;
024import javax.swing.text.JTextComponent;
025
026import org.openstreetmap.josm.data.Bounds;
027import org.openstreetmap.josm.data.coor.LatLon;
028import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
029import org.openstreetmap.josm.gui.widgets.JosmTextArea;
030import org.openstreetmap.josm.gui.widgets.JosmTextField;
031import org.openstreetmap.josm.tools.GBC;
032import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider;
033import org.openstreetmap.josm.tools.Logging;
034import org.openstreetmap.josm.tools.OsmUrlToBounds;
035
036/**
037 * Bounding box selector.
038 *
039 * Provides max/min lat/lon input fields as well as the "URL from www.openstreetmap.org" text field.
040 *
041 * @author Frederik Ramm
042 *
043 */
044public class BoundingBoxSelection implements DownloadSelection {
045
046    private JosmTextField[] latlon;
047    private final JosmTextArea tfOsmUrl = new JosmTextArea();
048    private final JosmTextArea showUrl = new JosmTextArea();
049    private DownloadDialog parent;
050
051    protected void registerBoundingBoxBuilder() {
052        BoundingBoxBuilder bboxbuilder = new BoundingBoxBuilder();
053        for (JosmTextField ll : latlon) {
054            ll.addFocusListener(bboxbuilder);
055            ll.addActionListener(bboxbuilder);
056        }
057    }
058
059    protected void buildDownloadAreaInputFields() {
060        latlon = new JosmTextField[4];
061        for (int i = 0; i < 4; i++) {
062            latlon[i] = new JosmTextField(11);
063            latlon[i].setMinimumSize(new Dimension(100, new JosmTextField().getMinimumSize().height));
064            latlon[i].addFocusListener(new SelectAllOnFocusHandler(latlon[i]));
065        }
066        LatValueChecker latChecker = new LatValueChecker(latlon[0]);
067        latlon[0].addFocusListener(latChecker);
068        latlon[0].addActionListener(latChecker);
069
070        latChecker = new LatValueChecker(latlon[2]);
071        latlon[2].addFocusListener(latChecker);
072        latlon[2].addActionListener(latChecker);
073
074        LonValueChecker lonChecker = new LonValueChecker(latlon[1]);
075        latlon[1].addFocusListener(lonChecker);
076        latlon[1].addActionListener(lonChecker);
077
078        lonChecker = new LonValueChecker(latlon[3]);
079        latlon[3].addFocusListener(lonChecker);
080        latlon[3].addActionListener(lonChecker);
081
082        registerBoundingBoxBuilder();
083    }
084
085    @Override
086    public void addGui(final DownloadDialog gui) {
087        buildDownloadAreaInputFields();
088        final JPanel dlg = new JPanel(new GridBagLayout());
089
090        tfOsmUrl.getDocument().addDocumentListener(new OsmUrlRefresher());
091
092        // select content on receiving focus. this seems to be the default in the
093        // windows look+feel but not for others. needs invokeLater to avoid strange
094        // side effects that will cancel out the newly made selection otherwise.
095        tfOsmUrl.addFocusListener(new SelectAllOnFocusHandler(tfOsmUrl));
096        tfOsmUrl.setLineWrap(true);
097        tfOsmUrl.setBorder(latlon[0].getBorder());
098
099        dlg.add(new JLabel(tr("min lat")), GBC.std().insets(10, 20, 5, 0));
100        dlg.add(latlon[0], GBC.std().insets(0, 20, 0, 0));
101        dlg.add(new JLabel(tr("min lon")), GBC.std().insets(10, 20, 5, 0));
102        dlg.add(latlon[1], GBC.eol().insets(0, 20, 0, 0));
103        dlg.add(new JLabel(tr("max lat")), GBC.std().insets(10, 0, 5, 0));
104        dlg.add(latlon[2], GBC.std());
105        dlg.add(new JLabel(tr("max lon")), GBC.std().insets(10, 0, 5, 0));
106        dlg.add(latlon[3], GBC.eol());
107
108        final JButton btnClear = new JButton(tr("Clear textarea"));
109        btnClear.addMouseListener(new MouseAdapter() {
110            @Override
111            public void mouseClicked(MouseEvent arg0) {
112                tfOsmUrl.setText("");
113            }
114        });
115        dlg.add(btnClear, GBC.eol().insets(10, 20, 0, 0));
116
117        dlg.add(new JLabel(tr("URL from www.openstreetmap.org (you can paste an URL here to download the area)")),
118                GBC.eol().insets(10, 5, 5, 0));
119        dlg.add(tfOsmUrl, GBC.eop().insets(10, 0, 5, 0).fill());
120        dlg.add(showUrl, GBC.eop().insets(10, 0, 5, 5));
121        showUrl.setEditable(false);
122        showUrl.setBackground(dlg.getBackground());
123        showUrl.addFocusListener(new SelectAllOnFocusHandler(showUrl));
124
125        if (gui != null)
126            gui.addDownloadAreaSelector(dlg, tr("Bounding Box"));
127        this.parent = gui;
128    }
129
130    @Override
131    public void setDownloadArea(Bounds area) {
132        updateBboxFields(area);
133        updateUrl(area);
134    }
135
136    /**
137     * Replies the download area.
138     * @return The download area
139     */
140    public Bounds getDownloadArea() {
141        double[] values = new double[4];
142        for (int i = 0; i < 4; i++) {
143            try {
144                values[i] = JosmDecimalFormatSymbolsProvider.parseDouble(latlon[i].getText());
145            } catch (NumberFormatException ex) {
146                return null;
147            }
148        }
149        if (!LatLon.isValidLat(values[0]) || !LatLon.isValidLon(values[1]))
150            return null;
151        if (!LatLon.isValidLat(values[2]) || !LatLon.isValidLon(values[3]))
152            return null;
153        return new Bounds(values);
154    }
155
156    private boolean parseURL(DownloadDialog gui) {
157        Bounds b = OsmUrlToBounds.parse(tfOsmUrl.getText());
158        if (b == null) return false;
159        gui.boundingBoxChanged(b, this);
160        updateBboxFields(b);
161        updateUrl(b);
162        return true;
163    }
164
165    private void updateBboxFields(Bounds area) {
166        if (area == null) return;
167        latlon[0].setText(DecimalDegreesCoordinateFormat.INSTANCE.latToString(area.getMin()));
168        latlon[1].setText(DecimalDegreesCoordinateFormat.INSTANCE.lonToString(area.getMin()));
169        latlon[2].setText(DecimalDegreesCoordinateFormat.INSTANCE.latToString(area.getMax()));
170        latlon[3].setText(DecimalDegreesCoordinateFormat.INSTANCE.lonToString(area.getMax()));
171        for (JosmTextField tf: latlon) {
172            resetErrorMessage(tf);
173        }
174    }
175
176    private void updateUrl(Bounds area) {
177        if (area == null) return;
178        showUrl.setText(OsmUrlToBounds.getURL(area));
179    }
180
181    private final Border errorBorder = BorderFactory.createLineBorder(Color.RED, 1);
182
183    protected void setErrorMessage(JosmTextField tf, String msg) {
184        tf.setBorder(errorBorder);
185        tf.setToolTipText(msg);
186    }
187
188    protected void resetErrorMessage(JosmTextField tf) {
189        tf.setBorder(UIManager.getBorder("TextField.border"));
190        tf.setToolTipText(null);
191    }
192
193    class LatValueChecker extends FocusAdapter implements ActionListener {
194        private final JosmTextField tfLatValue;
195
196        LatValueChecker(JosmTextField tfLatValue) {
197            this.tfLatValue = tfLatValue;
198        }
199
200        protected void check() {
201            double value = 0;
202            try {
203                value = JosmDecimalFormatSymbolsProvider.parseDouble(tfLatValue.getText());
204            } catch (NumberFormatException ex) {
205                setErrorMessage(tfLatValue, tr("The string ''{0}'' is not a valid double value.", tfLatValue.getText()));
206                return;
207            }
208            if (!LatLon.isValidLat(value)) {
209                setErrorMessage(tfLatValue, tr("Value for latitude in range [-90,90] required.", tfLatValue.getText()));
210                return;
211            }
212            resetErrorMessage(tfLatValue);
213        }
214
215        @Override
216        public void focusLost(FocusEvent e) {
217            check();
218        }
219
220        @Override
221        public void actionPerformed(ActionEvent e) {
222            check();
223        }
224    }
225
226    class LonValueChecker extends FocusAdapter implements ActionListener {
227        private final JosmTextField tfLonValue;
228
229        LonValueChecker(JosmTextField tfLonValue) {
230            this.tfLonValue = tfLonValue;
231        }
232
233        protected void check() {
234            double value = 0;
235            try {
236                value = JosmDecimalFormatSymbolsProvider.parseDouble(tfLonValue.getText());
237            } catch (NumberFormatException ex) {
238                setErrorMessage(tfLonValue, tr("The string ''{0}'' is not a valid double value.", tfLonValue.getText()));
239                return;
240            }
241            if (!LatLon.isValidLon(value)) {
242                setErrorMessage(tfLonValue, tr("Value for longitude in range [-180,180] required.", tfLonValue.getText()));
243                return;
244            }
245            resetErrorMessage(tfLonValue);
246        }
247
248        @Override
249        public void focusLost(FocusEvent e) {
250            check();
251        }
252
253        @Override
254        public void actionPerformed(ActionEvent e) {
255            check();
256        }
257    }
258
259    static class SelectAllOnFocusHandler extends FocusAdapter {
260        private final JTextComponent tfTarget;
261
262        SelectAllOnFocusHandler(JTextComponent tfTarget) {
263            this.tfTarget = tfTarget;
264        }
265
266        @Override
267        public void focusGained(FocusEvent e) {
268            tfTarget.selectAll();
269        }
270    }
271
272    class OsmUrlRefresher implements DocumentListener {
273        @Override
274        public void changedUpdate(DocumentEvent e) {
275            parseURL(parent);
276        }
277
278        @Override
279        public void insertUpdate(DocumentEvent e) {
280            parseURL(parent);
281        }
282
283        @Override
284        public void removeUpdate(DocumentEvent e) {
285            parseURL(parent);
286        }
287    }
288
289    class BoundingBoxBuilder extends FocusAdapter implements ActionListener {
290        protected Bounds build() {
291            double minlon, minlat, maxlon, maxlat;
292            try {
293                minlat = JosmDecimalFormatSymbolsProvider.parseDouble(latlon[0].getText().trim());
294                minlon = JosmDecimalFormatSymbolsProvider.parseDouble(latlon[1].getText().trim());
295                maxlat = JosmDecimalFormatSymbolsProvider.parseDouble(latlon[2].getText().trim());
296                maxlon = JosmDecimalFormatSymbolsProvider.parseDouble(latlon[3].getText().trim());
297            } catch (NumberFormatException e) {
298                Logging.trace(e);
299                return null;
300            }
301            if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)
302                    || !LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat))
303                return null;
304            if (minlon > maxlon)
305                return null;
306            if (minlat > maxlat)
307                return null;
308            return new Bounds(minlat, minlon, maxlat, maxlon);
309        }
310
311        protected void refreshBounds() {
312            Bounds b = build();
313            parent.boundingBoxChanged(b, BoundingBoxSelection.this);
314        }
315
316        @Override
317        public void focusLost(FocusEvent e) {
318            refreshBounds();
319        }
320
321        @Override
322        public void actionPerformed(ActionEvent e) {
323            refreshBounds();
324        }
325    }
326}