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