001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.bbox; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.AWTKeyStroke; 007import java.awt.BorderLayout; 008import java.awt.Color; 009import java.awt.FlowLayout; 010import java.awt.Graphics; 011import java.awt.GridBagConstraints; 012import java.awt.GridBagLayout; 013import java.awt.Insets; 014import java.awt.KeyboardFocusManager; 015import java.awt.Point; 016import java.awt.event.ActionEvent; 017import java.awt.event.ActionListener; 018import java.awt.event.FocusEvent; 019import java.awt.event.FocusListener; 020import java.awt.event.KeyEvent; 021import java.beans.PropertyChangeEvent; 022import java.beans.PropertyChangeListener; 023import java.util.ArrayList; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Set; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029 030import javax.swing.AbstractAction; 031import javax.swing.BorderFactory; 032import javax.swing.JButton; 033import javax.swing.JLabel; 034import javax.swing.JPanel; 035import javax.swing.JSpinner; 036import javax.swing.KeyStroke; 037import javax.swing.SpinnerNumberModel; 038import javax.swing.event.ChangeEvent; 039import javax.swing.event.ChangeListener; 040import javax.swing.text.JTextComponent; 041 042import org.openstreetmap.gui.jmapviewer.JMapViewer; 043import org.openstreetmap.gui.jmapviewer.MapMarkerDot; 044import org.openstreetmap.gui.jmapviewer.OsmMercator; 045import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 046import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; 047import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 048import org.openstreetmap.josm.data.Bounds; 049import org.openstreetmap.josm.data.Version; 050import org.openstreetmap.josm.data.coor.LatLon; 051import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 052import org.openstreetmap.josm.gui.widgets.HtmlPanel; 053import org.openstreetmap.josm.gui.widgets.JosmTextField; 054import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator; 055import org.openstreetmap.josm.tools.ImageProvider; 056 057/** 058 * TileSelectionBBoxChooser allows to select a bounding box (i.e. for downloading) based 059 * on OSM tile numbers. 060 * 061 * TileSelectionBBoxChooser can be embedded as component in a Swing container. Example: 062 * <pre> 063 * JFrame f = new JFrame(....); 064 * f.getContentPane().setLayout(new BorderLayout())); 065 * TileSelectionBBoxChooser chooser = new TileSelectionBBoxChooser(); 066 * f.add(chooser, BorderLayout.CENTER); 067 * chooser.addPropertyChangeListener(new PropertyChangeListener() { 068 * public void propertyChange(PropertyChangeEvent evt) { 069 * // listen for BBOX events 070 * if (evt.getPropertyName().equals(BBoxChooser.BBOX_PROP)) { 071 * Main.info("new bbox based on OSM tiles selected: " + (Bounds)evt.getNewValue()); 072 * } 073 * } 074 * }); 075 * 076 * // init the chooser with a bounding box 077 * chooser.setBoundingBox(....); 078 * 079 * f.setVisible(true); 080 * </pre> 081 */ 082public class TileSelectionBBoxChooser extends JPanel implements BBoxChooser{ 083 084 /** the current bounding box */ 085 private Bounds bbox; 086 /** the map viewer showing the selected bounding box */ 087 private TileBoundsMapView mapViewer; 088 /** a panel for entering a bounding box given by a tile grid and a zoom level */ 089 private TileGridInputPanel pnlTileGrid; 090 /** a panel for entering a bounding box given by the address of an individual OSM tile at 091 * a given zoom level 092 */ 093 private TileAddressInputPanel pnlTileAddress; 094 095 /** 096 * builds the UI 097 */ 098 protected void build() { 099 setLayout(new GridBagLayout()); 100 101 GridBagConstraints gc = new GridBagConstraints(); 102 gc.weightx = 0.5; 103 gc.fill = GridBagConstraints.HORIZONTAL; 104 gc.anchor = GridBagConstraints.NORTHWEST; 105 add(pnlTileGrid = new TileGridInputPanel(), gc); 106 107 gc.gridx = 1; 108 add(pnlTileAddress = new TileAddressInputPanel(), gc); 109 110 gc.gridx = 0; 111 gc.gridy = 1; 112 gc.gridwidth = 2; 113 gc.weightx = 1.0; 114 gc.weighty = 1.0; 115 gc.fill = GridBagConstraints.BOTH; 116 gc.insets = new Insets(2,2,2,2); 117 add(mapViewer = new TileBoundsMapView(), gc); 118 mapViewer.setFocusable(false); 119 mapViewer.setZoomContolsVisible(false); 120 mapViewer.setMapMarkerVisible(false); 121 122 pnlTileAddress.addPropertyChangeListener(pnlTileGrid); 123 pnlTileGrid.addPropertyChangeListener(new TileBoundsChangeListener()); 124 } 125 126 /** 127 * Constructs a new {@code TileSelectionBBoxChooser}. 128 */ 129 public TileSelectionBBoxChooser() { 130 build(); 131 } 132 133 /** 134 * Replies the current bounding box. null, if no valid bounding box is currently selected. 135 * 136 */ 137 @Override 138 public Bounds getBoundingBox() { 139 return bbox; 140 } 141 142 /** 143 * Sets the current bounding box. 144 * 145 * @param bbox the bounding box. null, if this widget isn't initialized with a bounding box 146 */ 147 @Override 148 public void setBoundingBox(Bounds bbox) { 149 pnlTileGrid.initFromBoundingBox(bbox); 150 } 151 152 protected void refreshMapView() { 153 if (bbox == null) return; 154 155 // calc the screen coordinates for the new selection rectangle 156 MapMarkerDot xmin_ymin = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon()); 157 MapMarkerDot xmax_ymax = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon()); 158 159 List<MapMarker> marker = new ArrayList<MapMarker>(2); 160 marker.add(xmin_ymin); 161 marker.add(xmax_ymax); 162 mapViewer.setBoundingBox(bbox); 163 mapViewer.setMapMarkerList(marker); 164 mapViewer.setDisplayToFitMapMarkers(); 165 mapViewer.zoomOut(); 166 } 167 168 /** 169 * Computes the bounding box given a tile grid. 170 * 171 * @param tb the description of the tile grid 172 * @return the bounding box 173 */ 174 protected Bounds convertTileBoundsToBoundingBox(TileBounds tb) { 175 LatLon min = getNorthWestLatLonOfTile(tb.min, tb.zoomLevel); 176 Point p = new Point(tb.max); 177 p.x++; 178 p.y++; 179 LatLon max = getNorthWestLatLonOfTile(p, tb.zoomLevel); 180 return new Bounds(max.lat(), min.lon(), min.lat(), max.lon()); 181 } 182 183 /** 184 * Replies lat/lon of the north/west-corner of a tile at a specific zoom level 185 * 186 * @param tile the tile address (x,y) 187 * @param zoom the zoom level 188 * @return lat/lon of the north/west-corner of a tile at a specific zoom level 189 */ 190 protected LatLon getNorthWestLatLonOfTile(Point tile, int zoom) { 191 double lon = tile.x / Math.pow(2.0, zoom) * 360.0 - 180; 192 double lat = Math.toDegrees(Math.atan(Math.sinh(Math.PI - (2.0 * Math.PI * tile.y) / Math.pow(2.0, zoom)))); 193 return new LatLon(lat, lon); 194 } 195 196 /** 197 * Listens to changes in the selected tile bounds, refreshes the map view and emits 198 * property change events for {@link BBoxChooser#BBOX_PROP} 199 */ 200 class TileBoundsChangeListener implements PropertyChangeListener { 201 @Override 202 public void propertyChange(PropertyChangeEvent evt) { 203 if (!evt.getPropertyName().equals(TileGridInputPanel.TILE_BOUNDS_PROP)) return; 204 TileBounds tb = (TileBounds)evt.getNewValue(); 205 Bounds oldValue = TileSelectionBBoxChooser.this.bbox; 206 TileSelectionBBoxChooser.this.bbox = convertTileBoundsToBoundingBox(tb); 207 firePropertyChange(BBOX_PROP, oldValue, TileSelectionBBoxChooser.this.bbox); 208 refreshMapView(); 209 } 210 } 211 212 /** 213 * A panel for describing a rectangular area of OSM tiles at a given zoom level. 214 * 215 * The panel emits PropertyChangeEvents for the property {@link TileGridInputPanel#TILE_BOUNDS_PROP} 216 * when the user successfully enters a valid tile grid specification. 217 * 218 */ 219 static private class TileGridInputPanel extends JPanel implements PropertyChangeListener{ 220 static public final String TILE_BOUNDS_PROP = TileGridInputPanel.class.getName() + ".tileBounds"; 221 222 private JosmTextField tfMaxY; 223 private JosmTextField tfMinY; 224 private JosmTextField tfMaxX; 225 private JosmTextField tfMinX; 226 private TileCoordinateValidator valMaxY; 227 private TileCoordinateValidator valMinY; 228 private TileCoordinateValidator valMaxX; 229 private TileCoordinateValidator valMinX; 230 private JSpinner spZoomLevel; 231 private TileBoundsBuilder tileBoundsBuilder = new TileBoundsBuilder(); 232 private boolean doFireTileBoundChanged = true; 233 234 protected JPanel buildTextPanel() { 235 JPanel pnl = new JPanel(new BorderLayout()); 236 HtmlPanel msg = new HtmlPanel(); 237 msg.setText(tr("<html>Please select a <strong>range of OSM tiles</strong> at a given zoom level.</html>")); 238 pnl.add(msg); 239 return pnl; 240 } 241 242 protected JPanel buildZoomLevelPanel() { 243 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); 244 pnl.add(new JLabel(tr("Zoom level:"))); 245 pnl.add(spZoomLevel = new JSpinner(new SpinnerNumberModel(0,0,18,1))); 246 spZoomLevel.addChangeListener(new ZomeLevelChangeHandler()); 247 spZoomLevel.addChangeListener(tileBoundsBuilder); 248 return pnl; 249 } 250 251 protected JPanel buildTileGridInputPanel() { 252 JPanel pnl = new JPanel(new GridBagLayout()); 253 pnl.setBorder(BorderFactory.createEmptyBorder(2,2,2,2)); 254 GridBagConstraints gc = new GridBagConstraints(); 255 gc.anchor = GridBagConstraints.NORTHWEST; 256 gc.insets = new Insets(0, 0, 2, 2); 257 258 gc.gridwidth = 2; 259 gc.gridx = 1; 260 gc.fill = GridBagConstraints.HORIZONTAL; 261 pnl.add(buildZoomLevelPanel(), gc); 262 263 gc.gridwidth = 1; 264 gc.gridy = 1; 265 gc.gridx = 1; 266 pnl.add(new JLabel(tr("from tile")), gc); 267 268 gc.gridx = 2; 269 pnl.add(new JLabel(tr("up to tile")), gc); 270 271 gc.gridx = 0; 272 gc.gridy = 2; 273 gc.weightx = 0.0; 274 pnl.add(new JLabel("X:"), gc); 275 276 277 gc.gridx = 1; 278 gc.weightx = 0.5; 279 pnl.add(tfMinX = new JosmTextField(), gc); 280 valMinX = new TileCoordinateValidator(tfMinX); 281 SelectAllOnFocusGainedDecorator.decorate(tfMinX); 282 tfMinX.addActionListener(tileBoundsBuilder); 283 tfMinX.addFocusListener(tileBoundsBuilder); 284 285 gc.gridx = 2; 286 gc.weightx = 0.5; 287 pnl.add(tfMaxX = new JosmTextField(), gc); 288 valMaxX = new TileCoordinateValidator(tfMaxX); 289 SelectAllOnFocusGainedDecorator.decorate(tfMaxX); 290 tfMaxX.addActionListener(tileBoundsBuilder); 291 tfMaxX.addFocusListener(tileBoundsBuilder); 292 293 gc.gridx = 0; 294 gc.gridy = 3; 295 gc.weightx = 0.0; 296 pnl.add(new JLabel("Y:"), gc); 297 298 gc.gridx = 1; 299 gc.weightx = 0.5; 300 pnl.add(tfMinY = new JosmTextField(), gc); 301 valMinY = new TileCoordinateValidator(tfMinY); 302 SelectAllOnFocusGainedDecorator.decorate(tfMinY); 303 tfMinY.addActionListener(tileBoundsBuilder); 304 tfMinY.addFocusListener(tileBoundsBuilder); 305 306 gc.gridx = 2; 307 gc.weightx = 0.5; 308 pnl.add(tfMaxY = new JosmTextField(), gc); 309 valMaxY = new TileCoordinateValidator(tfMaxY); 310 SelectAllOnFocusGainedDecorator.decorate(tfMaxY); 311 tfMaxY.addActionListener(tileBoundsBuilder); 312 tfMaxY.addFocusListener(tileBoundsBuilder); 313 314 gc.gridy = 4; 315 gc.gridx = 0; 316 gc.gridwidth = 3; 317 gc.weightx = 1.0; 318 gc.weighty = 1.0; 319 gc.fill = GridBagConstraints.BOTH; 320 pnl.add(new JPanel(), gc); 321 return pnl; 322 } 323 324 protected void build() { 325 setLayout(new BorderLayout()); 326 setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 327 add(buildTextPanel(), BorderLayout.NORTH); 328 add(buildTileGridInputPanel(), BorderLayout.CENTER); 329 330 Set<AWTKeyStroke> forwardKeys = new HashSet<AWTKeyStroke>(getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)); 331 forwardKeys.add(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); 332 setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,forwardKeys); 333 } 334 335 public TileGridInputPanel() { 336 build(); 337 } 338 339 public void initFromBoundingBox(Bounds bbox) { 340 if (bbox == null) 341 return; 342 TileBounds tb = new TileBounds(); 343 tb.zoomLevel = (Integer) spZoomLevel.getValue(); 344 tb.min = new Point( 345 Math.max(0,lonToTileX(tb.zoomLevel, bbox.getMinLon())), 346 Math.max(0,latToTileY(tb.zoomLevel, bbox.getMaxLat() - 0.00001)) 347 ); 348 tb.max = new Point( 349 Math.max(0,lonToTileX(tb.zoomLevel, bbox.getMaxLon())), 350 Math.max(0,latToTileY(tb.zoomLevel, bbox.getMinLat() - 0.00001)) 351 ); 352 doFireTileBoundChanged = false; 353 setTileBounds(tb); 354 doFireTileBoundChanged = true; 355 } 356 357 public static int latToTileY(int zoom, double lat) { 358 if ((zoom < 3) || (zoom > 18)) return -1; 359 double l = lat / 180 * Math.PI; 360 double pf = Math.log(Math.tan(l) + (1/Math.cos(l))); 361 return (int) ((1<<(zoom-1)) * (Math.PI - pf) / Math.PI); 362 } 363 364 public static int lonToTileX(int zoom, double lon) { 365 if ((zoom < 3) || (zoom > 18)) return -1; 366 return (int) ((1<<(zoom-3)) * (lon + 180.0) / 45.0); 367 } 368 369 public void setTileBounds(TileBounds tileBounds) { 370 tfMinX.setText(Integer.toString(tileBounds.min.x)); 371 tfMinY.setText(Integer.toString(tileBounds.min.y)); 372 tfMaxX.setText(Integer.toString(tileBounds.max.x)); 373 tfMaxY.setText(Integer.toString(tileBounds.max.y)); 374 spZoomLevel.setValue(tileBounds.zoomLevel); 375 } 376 377 @Override 378 public void propertyChange(PropertyChangeEvent evt) { 379 if (evt.getPropertyName().equals(TileAddressInputPanel.TILE_BOUNDS_PROP)) { 380 TileBounds tb = (TileBounds)evt.getNewValue(); 381 setTileBounds(tb); 382 fireTileBoundsChanged(tb); 383 } 384 } 385 386 protected void fireTileBoundsChanged(TileBounds tb) { 387 if (!doFireTileBoundChanged) return; 388 firePropertyChange(TILE_BOUNDS_PROP, null, tb); 389 } 390 391 class ZomeLevelChangeHandler implements ChangeListener { 392 @Override 393 public void stateChanged(ChangeEvent e) { 394 int zoomLevel = (Integer)spZoomLevel.getValue(); 395 valMaxX.setZoomLevel(zoomLevel); 396 valMaxY.setZoomLevel(zoomLevel); 397 valMinX.setZoomLevel(zoomLevel); 398 valMinY.setZoomLevel(zoomLevel); 399 } 400 } 401 402 class TileBoundsBuilder implements ActionListener, FocusListener, ChangeListener { 403 protected void buildTileBounds() { 404 if (!valMaxX.isValid()) return; 405 if (!valMaxY.isValid()) return; 406 if (!valMinX.isValid()) return; 407 if (!valMinY.isValid()) return; 408 Point min = new Point(valMinX.getTileIndex(), valMinY.getTileIndex()); 409 Point max = new Point(valMaxX.getTileIndex(), valMaxY.getTileIndex()); 410 int zoomlevel = (Integer)spZoomLevel.getValue(); 411 TileBounds tb = new TileBounds(min, max, zoomlevel); 412 fireTileBoundsChanged(tb); 413 } 414 415 @Override 416 public void focusGained(FocusEvent e) {/* irrelevant */} 417 418 @Override 419 public void focusLost(FocusEvent e) { 420 buildTileBounds(); 421 } 422 423 @Override 424 public void actionPerformed(ActionEvent e) { 425 buildTileBounds(); 426 } 427 428 @Override 429 public void stateChanged(ChangeEvent e) { 430 buildTileBounds(); 431 } 432 } 433 } 434 435 /** 436 * A panel for entering the address of a single OSM tile at a given zoom level. 437 * 438 */ 439 static private class TileAddressInputPanel extends JPanel { 440 441 static public final String TILE_BOUNDS_PROP = TileAddressInputPanel.class.getName() + ".tileBounds"; 442 443 private JosmTextField tfTileAddress; 444 private TileAddressValidator valTileAddress; 445 446 protected JPanel buildTextPanel() { 447 JPanel pnl = new JPanel(new BorderLayout()); 448 HtmlPanel msg = new HtmlPanel(); 449 msg.setText(tr("<html>Alternatively you may enter a <strong>tile address</strong> for a single tile " 450 + "in the format <i>zoomlevel/x/y</i>, i.e. <i>15/256/223</i>. Tile addresses " 451 + "in the format <i>zoom,x,y</i> or <i>zoom;x;y</i> are valid too.</html>")); 452 pnl.add(msg); 453 return pnl; 454 } 455 456 protected JPanel buildTileAddressInputPanel() { 457 JPanel pnl = new JPanel(new GridBagLayout()); 458 GridBagConstraints gc = new GridBagConstraints(); 459 gc.anchor = GridBagConstraints.NORTHWEST; 460 gc.fill = GridBagConstraints.HORIZONTAL; 461 gc.weightx = 0.0; 462 gc.insets = new Insets(0,0,2,2); 463 pnl.add(new JLabel(tr("Tile address:")), gc); 464 465 gc.weightx = 1.0; 466 gc.gridx = 1; 467 pnl.add(tfTileAddress = new JosmTextField(), gc); 468 valTileAddress = new TileAddressValidator(tfTileAddress); 469 SelectAllOnFocusGainedDecorator.decorate(tfTileAddress); 470 471 gc.weightx = 0.0; 472 gc.gridx = 2; 473 ApplyTileAddressAction applyTileAddressAction = new ApplyTileAddressAction(); 474 JButton btn = new JButton(applyTileAddressAction); 475 btn.setBorder(BorderFactory.createEmptyBorder(1,1,1,1)); 476 pnl.add(btn, gc); 477 tfTileAddress.addActionListener(applyTileAddressAction); 478 return pnl; 479 } 480 481 protected void build() { 482 setLayout(new GridBagLayout()); 483 GridBagConstraints gc = new GridBagConstraints(); 484 gc.anchor = GridBagConstraints.NORTHWEST; 485 gc.fill = GridBagConstraints.HORIZONTAL; 486 gc.weightx = 1.0; 487 gc.insets = new Insets(0,0,5,0); 488 add(buildTextPanel(), gc); 489 490 gc.gridy = 1; 491 add(buildTileAddressInputPanel(), gc); 492 493 // filler - grab remaining space 494 gc.gridy = 2; 495 gc.fill = GridBagConstraints.BOTH; 496 gc.weighty = 1.0; 497 add(new JPanel(), gc); 498 } 499 500 public TileAddressInputPanel() { 501 setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 502 build(); 503 } 504 505 protected void fireTileBoundsChanged(TileBounds tb){ 506 firePropertyChange(TILE_BOUNDS_PROP, null, tb); 507 } 508 509 class ApplyTileAddressAction extends AbstractAction { 510 public ApplyTileAddressAction() { 511 putValue(SMALL_ICON, ImageProvider.get("apply")); 512 putValue(SHORT_DESCRIPTION, tr("Apply the tile address")); 513 } 514 515 @Override 516 public void actionPerformed(ActionEvent e) { 517 TileBounds tb = valTileAddress.getTileBounds(); 518 if (tb != null) { 519 fireTileBoundsChanged(tb); 520 } 521 } 522 } 523 } 524 525 /** 526 * Validates a tile address 527 */ 528 static private class TileAddressValidator extends AbstractTextComponentValidator { 529 530 private TileBounds tileBounds = null; 531 532 public TileAddressValidator(JTextComponent tc) throws IllegalArgumentException { 533 super(tc); 534 } 535 536 @Override 537 public boolean isValid() { 538 String value = getComponent().getText().trim(); 539 Matcher m = Pattern.compile("(\\d+)[^\\d]+(\\d+)[^\\d]+(\\d+)").matcher(value); 540 tileBounds = null; 541 if (!m.matches()) return false; 542 int zoom; 543 try { 544 zoom = Integer.parseInt(m.group(1)); 545 } catch(NumberFormatException e){ 546 return false; 547 } 548 if (zoom < 0 || zoom > 18) return false; 549 550 int x; 551 try { 552 x = Integer.parseInt(m.group(2)); 553 } catch(NumberFormatException e){ 554 return false; 555 } 556 if (x < 0 || x >= Math.pow(2, zoom)) return false; 557 int y; 558 try { 559 y = Integer.parseInt(m.group(3)); 560 } catch(NumberFormatException e){ 561 return false; 562 } 563 if (y < 0 || y >= Math.pow(2, zoom)) return false; 564 565 tileBounds = new TileBounds(new Point(x,y), new Point(x,y), zoom); 566 return true; 567 } 568 569 @Override 570 public void validate() { 571 if (isValid()) { 572 feedbackValid(tr("Please enter a tile address")); 573 } else { 574 feedbackInvalid(tr("The current value isn''t a valid tile address", getComponent().getText())); 575 } 576 } 577 578 public TileBounds getTileBounds() { 579 return tileBounds; 580 } 581 } 582 583 /** 584 * Validates the x- or y-coordinate of a tile at a given zoom level. 585 * 586 */ 587 static private class TileCoordinateValidator extends AbstractTextComponentValidator { 588 private int zoomLevel; 589 private int tileIndex; 590 591 public TileCoordinateValidator(JTextComponent tc) throws IllegalArgumentException { 592 super(tc); 593 } 594 595 public void setZoomLevel(int zoomLevel) { 596 this.zoomLevel = zoomLevel; 597 validate(); 598 } 599 600 @Override 601 public boolean isValid() { 602 String value = getComponent().getText().trim(); 603 try { 604 if (value.isEmpty()) { 605 tileIndex = 0; 606 } else { 607 tileIndex = Integer.parseInt(value); 608 } 609 } catch(NumberFormatException e) { 610 return false; 611 } 612 if (tileIndex < 0 || tileIndex >= Math.pow(2, zoomLevel)) return false; 613 614 return true; 615 } 616 617 @Override 618 public void validate() { 619 if (isValid()) { 620 feedbackValid(tr("Please enter a tile index")); 621 } else { 622 feedbackInvalid(tr("The current value isn''t a valid tile index for the given zoom level", getComponent().getText())); 623 } 624 } 625 626 public int getTileIndex() { 627 return tileIndex; 628 } 629 } 630 631 /** 632 * Represents a rectangular area of tiles at a given zoom level. 633 * 634 */ 635 static private class TileBounds { 636 public Point min; 637 public Point max; 638 public int zoomLevel; 639 640 public TileBounds() { 641 zoomLevel = 0; 642 min = new Point(0,0); 643 max = new Point(0,0); 644 } 645 646 public TileBounds(Point min, Point max, int zoomLevel) { 647 this.min = min; 648 this.max = max; 649 this.zoomLevel = zoomLevel; 650 } 651 652 @Override 653 public String toString() { 654 StringBuffer sb = new StringBuffer(); 655 sb.append("min=").append(min.x).append(",").append(min.y).append(","); 656 sb.append("max=").append(max.x).append(",").append(max.y).append(","); 657 sb.append("zoom=").append(zoomLevel); 658 return sb.toString(); 659 } 660 } 661 662 /** 663 * The map view used in this bounding box chooser 664 */ 665 static private class TileBoundsMapView extends JMapViewer { 666 private Point min; 667 private Point max; 668 669 public TileBoundsMapView() { 670 setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); 671 TileLoader loader = tileController.getTileLoader(); 672 if (loader instanceof OsmTileLoader) { 673 ((OsmTileLoader)loader).headers.put("User-Agent", Version.getInstance().getFullAgentString()); 674 } 675 } 676 677 public void setBoundingBox(Bounds bbox){ 678 if (bbox == null) { 679 min = null; 680 max = null; 681 } else { 682 int y1 = OsmMercator.LatToY(bbox.getMinLat(), MAX_ZOOM); 683 int y2 = OsmMercator.LatToY(bbox.getMaxLat(), MAX_ZOOM); 684 int x1 = OsmMercator.LonToX(bbox.getMinLon(), MAX_ZOOM); 685 int x2 = OsmMercator.LonToX(bbox.getMaxLon(), MAX_ZOOM); 686 687 min = new Point(Math.min(x1, x2), Math.min(y1, y2)); 688 max = new Point(Math.max(x1, x2), Math.max(y1, y2)); 689 } 690 repaint(); 691 } 692 693 protected Point getTopLeftCoordinates() { 694 return new Point(center.x - (getWidth() / 2), center.y - (getHeight() / 2)); 695 } 696 697 /** 698 * Draw the map. 699 */ 700 @Override 701 public void paint(Graphics g) { 702 try { 703 super.paint(g); 704 if (min == null || max == null) return; 705 int zoomDiff = MAX_ZOOM - zoom; 706 Point tlc = getTopLeftCoordinates(); 707 int x_min = (min.x >> zoomDiff) - tlc.x; 708 int y_min = (min.y >> zoomDiff) - tlc.y; 709 int x_max = (max.x >> zoomDiff) - tlc.x; 710 int y_max = (max.y >> zoomDiff) - tlc.y; 711 712 int w = x_max - x_min; 713 int h = y_max - y_min; 714 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f)); 715 g.fillRect(x_min, y_min, w, h); 716 717 g.setColor(Color.BLACK); 718 g.drawRect(x_min, y_min, w, h); 719 } catch (Exception e) { 720 e.printStackTrace(); 721 } 722 } 723 } 724}