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