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