001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.gui.download;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.Color;
007    import java.awt.Dimension;
008    import java.awt.GridBagLayout;
009    import java.awt.Toolkit;
010    import java.awt.datatransfer.FlavorEvent;
011    import java.awt.datatransfer.FlavorListener;
012    import java.awt.event.ActionEvent;
013    import java.awt.event.ActionListener;
014    import java.awt.event.FocusAdapter;
015    import java.awt.event.FocusEvent;
016    import java.awt.event.MouseAdapter;
017    import java.awt.event.MouseEvent;
018    
019    import javax.swing.AbstractAction;
020    import javax.swing.BorderFactory;
021    import javax.swing.JButton;
022    import javax.swing.JLabel;
023    import javax.swing.JPanel;
024    import javax.swing.JPopupMenu;
025    import javax.swing.JTextArea;
026    import javax.swing.JTextField;
027    import javax.swing.UIManager;
028    import javax.swing.border.Border;
029    import javax.swing.event.DocumentEvent;
030    import javax.swing.event.DocumentListener;
031    import javax.swing.text.JTextComponent;
032    
033    import org.openstreetmap.josm.data.Bounds;
034    import org.openstreetmap.josm.data.coor.CoordinateFormat;
035    import org.openstreetmap.josm.data.coor.LatLon;
036    import org.openstreetmap.josm.tools.GBC;
037    import org.openstreetmap.josm.tools.ImageProvider;
038    import org.openstreetmap.josm.tools.OsmUrlToBounds;
039    import org.openstreetmap.josm.tools.Utils;
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 <frederik@remote.org>
047     *
048     */
049    public class BoundingBoxSelection implements DownloadSelection {
050    
051        private JTextField[] latlon = null;
052        private final JTextArea tfOsmUrl = new JTextArea();
053        private final JTextArea showUrl = new JTextArea();
054        private DownloadDialog parent;
055    
056        protected void registerBoundingBoxBuilder() {
057            BoundingBoxBuilder bboxbuilder = new BoundingBoxBuilder();
058            for (int i = 0;i < latlon.length; i++) {
059                latlon[i].addFocusListener(bboxbuilder);
060                latlon[i].addActionListener(bboxbuilder);
061            }
062        }
063    
064        protected void buildDownloadAreaInputFields() {
065            latlon = new JTextField[4];
066            for(int i=0; i< 4; i++) {
067                latlon[i] = new JTextField(11);
068                latlon[i].setMinimumSize(new Dimension(100,new JTextField().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        public void addGui(final DownloadDialog gui) {
091            buildDownloadAreaInputFields();
092            final JPanel dlg = new JPanel(new GridBagLayout());
093    
094            tfOsmUrl.getDocument().addDocumentListener(new OsmUrlRefresher());
095    
096            // select content on receiving focus. this seems to be the default in the
097            // windows look+feel but not for others. needs invokeLater to avoid strange
098            // side effects that will cancel out the newly made selection otherwise.
099            tfOsmUrl.addFocusListener(new SelectAllOnFocusHandler(tfOsmUrl));
100            tfOsmUrl.setLineWrap(true);
101            tfOsmUrl.setBorder(latlon[0].getBorder());
102    
103            dlg.add(new JLabel(tr("min lat")), GBC.std().insets(10,20,5,0));
104            dlg.add(latlon[0], GBC.std().insets(0,20,0,0));
105            dlg.add(new JLabel(tr("min lon")), GBC.std().insets(10,20,5,0));
106            dlg.add(latlon[1], GBC.eol().insets(0,20,0,0));
107            dlg.add(new JLabel(tr("max lat")), GBC.std().insets(10,0,5,0));
108            dlg.add(latlon[2], GBC.std());
109            dlg.add(new JLabel(tr("max lon")), GBC.std().insets(10,0,5,0));
110            dlg.add(latlon[3], GBC.eol());
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)")), GBC.eol().insets(10,5,5,0));
122            dlg.add(tfOsmUrl, GBC.eop().insets(10,0,5,0).fill());
123            tfOsmUrl.addMouseListener(
124                    new MouseAdapter() {
125                        @Override
126                        public void mousePressed(MouseEvent e) {
127                            checkPopup(e);
128                        }
129    
130                        @Override
131                        public void mouseClicked(MouseEvent e) {
132                            checkPopup(e);
133                        }
134    
135                        @Override
136                        public void mouseReleased(MouseEvent e) {
137                            checkPopup(e);
138                        }
139    
140                        private void checkPopup(MouseEvent e) {
141                            if (e.isPopupTrigger()) {
142                                OsmUrlPopup popup = new OsmUrlPopup();
143                                popup.show(tfOsmUrl, e.getX(), e.getY());
144                            }
145                        }
146                    }
147            );
148            dlg.add(showUrl, GBC.eop().insets(10,0,5,5));
149            showUrl.setEditable(false);
150            showUrl.setBackground(dlg.getBackground());
151            showUrl.addFocusListener(new SelectAllOnFocusHandler(showUrl));
152    
153            gui.addDownloadAreaSelector(dlg, tr("Bounding Box"));
154            this.parent = gui;
155        }
156    
157        public void setDownloadArea(Bounds area) {
158            updateBboxFields(area);
159            updateUrl(area);
160        }
161    
162        public Bounds getDownloadArea() {
163            double[] values = new double[4];
164            for (int i=0; i < 4; i++) {
165                try {
166                    values[i] = Double.parseDouble(latlon[i].getText());
167                } catch(NumberFormatException x) {
168                    return null;
169                }
170            }
171            if (!LatLon.isValidLat(values[0]) || !LatLon.isValidLon(values[1]))
172                return null;
173            if (!LatLon.isValidLat(values[2]) || !LatLon.isValidLon(values[3]))
174                return null;
175            return new Bounds(values);
176        }
177    
178        private boolean parseURL(DownloadDialog gui) {
179            Bounds b = OsmUrlToBounds.parse(tfOsmUrl.getText());
180            if(b == null) return false;
181            gui.boundingBoxChanged(b,BoundingBoxSelection.this);
182            updateBboxFields(b);
183            updateUrl(b);
184            return true;
185        }
186    
187        private void updateBboxFields(Bounds area) {
188            if (area == null) return;
189            latlon[0].setText(area.getMin().latToString(CoordinateFormat.DECIMAL_DEGREES));
190            latlon[1].setText(area.getMin().lonToString(CoordinateFormat.DECIMAL_DEGREES));
191            latlon[2].setText(area.getMax().latToString(CoordinateFormat.DECIMAL_DEGREES));
192            latlon[3].setText(area.getMax().lonToString(CoordinateFormat.DECIMAL_DEGREES));
193            for (JTextField tf: latlon) {
194                resetErrorMessage(tf);
195            }
196        }
197    
198        private void updateUrl(Bounds area) {
199            if (area == null) return;
200            showUrl.setText(OsmUrlToBounds.getURL(area));
201        }
202    
203        private Border errorBorder = BorderFactory.createLineBorder(Color.RED, 1);
204    
205        protected void setErrorMessage(JTextField tf, String msg) {
206            tf.setBorder(errorBorder);
207            tf.setToolTipText(msg);
208        }
209    
210        protected void resetErrorMessage(JTextField tf) {
211            tf.setBorder(UIManager.getBorder("TextField.border"));
212            tf.setToolTipText("");
213        }
214    
215        class LatValueChecker extends FocusAdapter implements ActionListener{
216            private JTextField tfLatValue;
217    
218            public LatValueChecker(JTextField tfLatValue) {
219                this.tfLatValue = tfLatValue;
220            }
221    
222            protected void check() {
223                double value = 0;
224                try {
225                    value = Double.parseDouble(tfLatValue.getText());
226                } catch(NumberFormatException ex) {
227                    setErrorMessage(tfLatValue,tr("The string ''{0}'' is not a valid double value.", tfLatValue.getText()));
228                    return;
229                }
230                if (!LatLon.isValidLat(value)) {
231                    setErrorMessage(tfLatValue,tr("Value for latitude in range [-90,90] required.", tfLatValue.getText()));
232                    return;
233                }
234                resetErrorMessage(tfLatValue);
235            }
236    
237            @Override
238            public void focusLost(FocusEvent e) {
239                check();
240            }
241    
242            public void actionPerformed(ActionEvent e) {
243                check();
244            }
245        }
246    
247        class LonValueChecker extends FocusAdapter implements ActionListener {
248            private JTextField tfLonValue;
249    
250            public LonValueChecker(JTextField tfLonValue) {
251                this.tfLonValue = tfLonValue;
252            }
253    
254            protected void check() {
255                double value = 0;
256                try {
257                    value = Double.parseDouble(tfLonValue.getText());
258                } catch(NumberFormatException ex) {
259                    setErrorMessage(tfLonValue,tr("The string ''{0}'' is not a valid double value.", tfLonValue.getText()));
260                    return;
261                }
262                if (!LatLon.isValidLon(value)) {
263                    setErrorMessage(tfLonValue,tr("Value for longitude in range [-180,180] required.", tfLonValue.getText()));
264                    return;
265                }
266                resetErrorMessage(tfLonValue);
267            }
268    
269            @Override
270            public void focusLost(FocusEvent e) {
271                check();
272            }
273    
274            public void actionPerformed(ActionEvent e) {
275                check();
276            }
277        }
278    
279        static class SelectAllOnFocusHandler extends FocusAdapter {
280            private JTextComponent tfTarget;
281            public SelectAllOnFocusHandler(JTextComponent tfTarget) {
282                this.tfTarget = tfTarget;
283            }
284    
285            @Override
286            public void focusGained(FocusEvent e) {
287                tfTarget.selectAll();
288            }
289        }
290    
291        class OsmUrlRefresher implements DocumentListener {
292            public void changedUpdate(DocumentEvent e) { parseURL(parent); }
293            public void insertUpdate(DocumentEvent e) { parseURL(parent); }
294            public void removeUpdate(DocumentEvent e) { parseURL(parent); }
295        }
296    
297        class PasteUrlAction extends AbstractAction implements FlavorListener {
298    
299            public PasteUrlAction() {
300                putValue(NAME, tr("Paste"));
301                putValue(SMALL_ICON, ImageProvider.get("paste"));
302                putValue(SHORT_DESCRIPTION, tr("Paste URL from clipboard"));
303                Toolkit.getDefaultToolkit().getSystemClipboard().addFlavorListener(this);
304            }
305    
306            public void actionPerformed(ActionEvent e) {
307                String content = Utils.getClipboardContent();
308                if (content != null) {
309                    tfOsmUrl.setText(content);
310                }
311            }
312    
313            protected void updateEnabledState() {
314                setEnabled(Utils.getClipboardContent() != null);
315            }
316    
317            public void flavorsChanged(FlavorEvent e) {
318                updateEnabledState();
319            }
320        }
321    
322        class OsmUrlPopup extends JPopupMenu {
323            public OsmUrlPopup() {
324                add(new PasteUrlAction());
325            }
326        }
327    
328        class BoundingBoxBuilder extends FocusAdapter implements ActionListener {
329            protected Bounds build() {
330                double minlon, minlat, maxlon,maxlat;
331                try {
332                    minlat = Double.parseDouble(latlon[0].getText().trim());
333                    minlon = Double.parseDouble(latlon[1].getText().trim());
334                    maxlat = Double.parseDouble(latlon[2].getText().trim());
335                    maxlon = Double.parseDouble(latlon[3].getText().trim());
336                } catch(NumberFormatException e) {
337                    return null;
338                }
339                if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)
340                        || !LatLon.isValidLat(minlat) || ! LatLon.isValidLat(maxlat))
341                    return null;
342                if (minlon > maxlon)
343                    return null;
344                if (minlat > maxlat)
345                    return null;
346                return new Bounds(minlat,minlon,maxlat,maxlon);
347            }
348    
349            protected void refreshBounds() {
350                Bounds  b = build();
351                parent.boundingBoxChanged(b, BoundingBoxSelection.this);
352            }
353    
354            @Override
355            public void focusLost(FocusEvent e) {
356                refreshBounds();
357            }
358    
359            public void actionPerformed(ActionEvent e) {
360                refreshBounds();
361            }
362        }
363    }