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