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