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}