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