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