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 <frederik@remote.org> 040 * 041 */ 042public class BoundingBoxSelection implements DownloadSelection { 043 044 private JosmTextField[] latlon = null; 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)")), GBC.eol().insets(10,5,5,0)); 116 dlg.add(tfOsmUrl, GBC.eop().insets(10,0,5,0).fill()); 117 dlg.add(showUrl, GBC.eop().insets(10,0,5,5)); 118 showUrl.setEditable(false); 119 showUrl.setBackground(dlg.getBackground()); 120 showUrl.addFocusListener(new SelectAllOnFocusHandler(showUrl)); 121 122 gui.addDownloadAreaSelector(dlg, tr("Bounding Box")); 123 this.parent = gui; 124 } 125 126 @Override 127 public void setDownloadArea(Bounds area) { 128 updateBboxFields(area); 129 updateUrl(area); 130 } 131 132 /** 133 * Replies the download area. 134 * @return The download area 135 */ 136 public Bounds getDownloadArea() { 137 double[] values = new double[4]; 138 for (int i=0; i < 4; i++) { 139 try { 140 values[i] = Double.parseDouble(latlon[i].getText()); 141 } catch(NumberFormatException x) { 142 return null; 143 } 144 } 145 if (!LatLon.isValidLat(values[0]) || !LatLon.isValidLon(values[1])) 146 return null; 147 if (!LatLon.isValidLat(values[2]) || !LatLon.isValidLon(values[3])) 148 return null; 149 return new Bounds(values); 150 } 151 152 private boolean parseURL(DownloadDialog gui) { 153 Bounds b = OsmUrlToBounds.parse(tfOsmUrl.getText()); 154 if(b == null) return false; 155 gui.boundingBoxChanged(b,BoundingBoxSelection.this); 156 updateBboxFields(b); 157 updateUrl(b); 158 return true; 159 } 160 161 private void updateBboxFields(Bounds area) { 162 if (area == null) return; 163 latlon[0].setText(area.getMin().latToString(CoordinateFormat.DECIMAL_DEGREES)); 164 latlon[1].setText(area.getMin().lonToString(CoordinateFormat.DECIMAL_DEGREES)); 165 latlon[2].setText(area.getMax().latToString(CoordinateFormat.DECIMAL_DEGREES)); 166 latlon[3].setText(area.getMax().lonToString(CoordinateFormat.DECIMAL_DEGREES)); 167 for (JosmTextField tf: latlon) { 168 resetErrorMessage(tf); 169 } 170 } 171 172 private void updateUrl(Bounds area) { 173 if (area == null) return; 174 showUrl.setText(OsmUrlToBounds.getURL(area)); 175 } 176 177 private Border errorBorder = BorderFactory.createLineBorder(Color.RED, 1); 178 179 protected void setErrorMessage(JosmTextField tf, String msg) { 180 tf.setBorder(errorBorder); 181 tf.setToolTipText(msg); 182 } 183 184 protected void resetErrorMessage(JosmTextField tf) { 185 tf.setBorder(UIManager.getBorder("TextField.border")); 186 tf.setToolTipText(""); 187 } 188 189 class LatValueChecker extends FocusAdapter implements ActionListener{ 190 private JosmTextField tfLatValue; 191 192 public LatValueChecker(JosmTextField tfLatValue) { 193 this.tfLatValue = tfLatValue; 194 } 195 196 protected void check() { 197 double value = 0; 198 try { 199 value = Double.parseDouble(tfLatValue.getText()); 200 } catch(NumberFormatException ex) { 201 setErrorMessage(tfLatValue,tr("The string ''{0}'' is not a valid double value.", tfLatValue.getText())); 202 return; 203 } 204 if (!LatLon.isValidLat(value)) { 205 setErrorMessage(tfLatValue,tr("Value for latitude in range [-90,90] required.", tfLatValue.getText())); 206 return; 207 } 208 resetErrorMessage(tfLatValue); 209 } 210 211 @Override 212 public void focusLost(FocusEvent e) { 213 check(); 214 } 215 216 @Override 217 public void actionPerformed(ActionEvent e) { 218 check(); 219 } 220 } 221 222 class LonValueChecker extends FocusAdapter implements ActionListener { 223 private JosmTextField tfLonValue; 224 225 public LonValueChecker(JosmTextField tfLonValue) { 226 this.tfLonValue = tfLonValue; 227 } 228 229 protected void check() { 230 double value = 0; 231 try { 232 value = Double.parseDouble(tfLonValue.getText()); 233 } catch(NumberFormatException ex) { 234 setErrorMessage(tfLonValue,tr("The string ''{0}'' is not a valid double value.", tfLonValue.getText())); 235 return; 236 } 237 if (!LatLon.isValidLon(value)) { 238 setErrorMessage(tfLonValue,tr("Value for longitude in range [-180,180] required.", tfLonValue.getText())); 239 return; 240 } 241 resetErrorMessage(tfLonValue); 242 } 243 244 @Override 245 public void focusLost(FocusEvent e) { 246 check(); 247 } 248 249 @Override 250 public void actionPerformed(ActionEvent e) { 251 check(); 252 } 253 } 254 255 static class SelectAllOnFocusHandler extends FocusAdapter { 256 private JTextComponent tfTarget; 257 public SelectAllOnFocusHandler(JTextComponent tfTarget) { 258 this.tfTarget = tfTarget; 259 } 260 261 @Override 262 public void focusGained(FocusEvent e) { 263 tfTarget.selectAll(); 264 } 265 } 266 267 class OsmUrlRefresher implements DocumentListener { 268 @Override 269 public void changedUpdate(DocumentEvent e) { parseURL(parent); } 270 @Override 271 public void insertUpdate(DocumentEvent e) { parseURL(parent); } 272 @Override 273 public void removeUpdate(DocumentEvent e) { parseURL(parent); } 274 } 275 276 class BoundingBoxBuilder extends FocusAdapter implements ActionListener { 277 protected Bounds build() { 278 double minlon, minlat, maxlon,maxlat; 279 try { 280 minlat = Double.parseDouble(latlon[0].getText().trim()); 281 minlon = Double.parseDouble(latlon[1].getText().trim()); 282 maxlat = Double.parseDouble(latlon[2].getText().trim()); 283 maxlon = Double.parseDouble(latlon[3].getText().trim()); 284 } catch(NumberFormatException e) { 285 return null; 286 } 287 if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon) 288 || !LatLon.isValidLat(minlat) || ! LatLon.isValidLat(maxlat)) 289 return null; 290 if (minlon > maxlon) 291 return null; 292 if (minlat > maxlat) 293 return null; 294 return new Bounds(minlat,minlon,maxlat,maxlon); 295 } 296 297 protected void refreshBounds() { 298 Bounds b = build(); 299 parent.boundingBoxChanged(b, BoundingBoxSelection.this); 300 } 301 302 @Override 303 public void focusLost(FocusEvent e) { 304 refreshBounds(); 305 } 306 307 @Override 308 public void actionPerformed(ActionEvent e) { 309 refreshBounds(); 310 } 311 } 312}