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