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