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