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