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.Main; 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 transient 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 final 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<>(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 private static class TileGridInputPanel extends JPanel implements PropertyChangeListener { 220 public static 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 transient TileCoordinateValidator valMaxY; 227 private transient TileCoordinateValidator valMinY; 228 private transient TileCoordinateValidator valMaxX; 229 private transient TileCoordinateValidator valMinX; 230 private JSpinner spZoomLevel; 231 private transient 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<>(getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)); 331 forwardKeys.add(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); 332 setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forwardKeys); 333 } 334 335 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) { 417 /* irrelevant */ 418 } 419 420 @Override 421 public void focusLost(FocusEvent e) { 422 buildTileBounds(); 423 } 424 425 @Override 426 public void actionPerformed(ActionEvent e) { 427 buildTileBounds(); 428 } 429 430 @Override 431 public void stateChanged(ChangeEvent e) { 432 buildTileBounds(); 433 } 434 } 435 } 436 437 /** 438 * A panel for entering the address of a single OSM tile at a given zoom level. 439 * 440 */ 441 private static class TileAddressInputPanel extends JPanel { 442 443 public static final String TILE_BOUNDS_PROP = TileAddressInputPanel.class.getName() + ".tileBounds"; 444 445 private transient TileAddressValidator valTileAddress; 446 447 protected JPanel buildTextPanel() { 448 JPanel pnl = new JPanel(new BorderLayout()); 449 HtmlPanel msg = new HtmlPanel(); 450 msg.setText(tr("<html>Alternatively you may enter a <strong>tile address</strong> for a single tile " 451 + "in the format <i>zoomlevel/x/y</i>, e.g. <i>15/256/223</i>. Tile addresses " 452 + "in the format <i>zoom,x,y</i> or <i>zoom;x;y</i> are valid too.</html>")); 453 pnl.add(msg); 454 return pnl; 455 } 456 457 protected JPanel buildTileAddressInputPanel() { 458 JPanel pnl = new JPanel(new GridBagLayout()); 459 GridBagConstraints gc = new GridBagConstraints(); 460 gc.anchor = GridBagConstraints.NORTHWEST; 461 gc.fill = GridBagConstraints.HORIZONTAL; 462 gc.weightx = 0.0; 463 gc.insets = new Insets(0, 0, 2, 2); 464 pnl.add(new JLabel(tr("Tile address:")), gc); 465 466 gc.weightx = 1.0; 467 gc.gridx = 1; 468 JosmTextField tfTileAddress = new JosmTextField(); 469 pnl.add(tfTileAddress, gc); 470 valTileAddress = new TileAddressValidator(tfTileAddress); 471 SelectAllOnFocusGainedDecorator.decorate(tfTileAddress); 472 473 gc.weightx = 0.0; 474 gc.gridx = 2; 475 ApplyTileAddressAction applyTileAddressAction = new ApplyTileAddressAction(); 476 JButton btn = new JButton(applyTileAddressAction); 477 btn.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); 478 pnl.add(btn, gc); 479 tfTileAddress.addActionListener(applyTileAddressAction); 480 return pnl; 481 } 482 483 protected void build() { 484 setLayout(new GridBagLayout()); 485 GridBagConstraints gc = new GridBagConstraints(); 486 gc.anchor = GridBagConstraints.NORTHWEST; 487 gc.fill = GridBagConstraints.HORIZONTAL; 488 gc.weightx = 1.0; 489 gc.insets = new Insets(0, 0, 5, 0); 490 add(buildTextPanel(), gc); 491 492 gc.gridy = 1; 493 add(buildTileAddressInputPanel(), gc); 494 495 // filler - grab remaining space 496 gc.gridy = 2; 497 gc.fill = GridBagConstraints.BOTH; 498 gc.weighty = 1.0; 499 add(new JPanel(), gc); 500 } 501 502 TileAddressInputPanel() { 503 setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 504 build(); 505 } 506 507 protected void fireTileBoundsChanged(TileBounds tb) { 508 firePropertyChange(TILE_BOUNDS_PROP, null, tb); 509 } 510 511 class ApplyTileAddressAction extends AbstractAction { 512 ApplyTileAddressAction() { 513 putValue(SMALL_ICON, ImageProvider.get("apply")); 514 putValue(SHORT_DESCRIPTION, tr("Apply the tile address")); 515 } 516 517 @Override 518 public void actionPerformed(ActionEvent e) { 519 TileBounds tb = valTileAddress.getTileBounds(); 520 if (tb != null) { 521 fireTileBoundsChanged(tb); 522 } 523 } 524 } 525 } 526 527 /** 528 * Validates a tile address 529 */ 530 private static class TileAddressValidator extends AbstractTextComponentValidator { 531 532 private TileBounds tileBounds; 533 534 TileAddressValidator(JTextComponent tc) { 535 super(tc); 536 } 537 538 @Override 539 public boolean isValid() { 540 String value = getComponent().getText().trim(); 541 Matcher m = Pattern.compile("(\\d+)[^\\d]+(\\d+)[^\\d]+(\\d+)").matcher(value); 542 tileBounds = null; 543 if (!m.matches()) return false; 544 int zoom; 545 try { 546 zoom = Integer.parseInt(m.group(1)); 547 } catch (NumberFormatException e) { 548 return false; 549 } 550 if (zoom < 0 || zoom > 18) return false; 551 552 int x; 553 try { 554 x = Integer.parseInt(m.group(2)); 555 } catch (NumberFormatException e) { 556 return false; 557 } 558 if (x < 0 || x >= Math.pow(2, zoom)) return false; 559 int y; 560 try { 561 y = Integer.parseInt(m.group(3)); 562 } catch (NumberFormatException e) { 563 return false; 564 } 565 if (y < 0 || y >= Math.pow(2, zoom)) return false; 566 567 tileBounds = new TileBounds(new Point(x, y), new Point(x, y), zoom); 568 return true; 569 } 570 571 @Override 572 public void validate() { 573 if (isValid()) { 574 feedbackValid(tr("Please enter a tile address")); 575 } else { 576 feedbackInvalid(tr("The current value isn''t a valid tile address", getComponent().getText())); 577 } 578 } 579 580 public TileBounds getTileBounds() { 581 return tileBounds; 582 } 583 } 584 585 /** 586 * Validates the x- or y-coordinate of a tile at a given zoom level. 587 * 588 */ 589 private static class TileCoordinateValidator extends AbstractTextComponentValidator { 590 private int zoomLevel; 591 private int tileIndex; 592 593 TileCoordinateValidator(JTextComponent tc) { 594 super(tc); 595 } 596 597 public void setZoomLevel(int zoomLevel) { 598 this.zoomLevel = zoomLevel; 599 validate(); 600 } 601 602 @Override 603 public boolean isValid() { 604 String value = getComponent().getText().trim(); 605 try { 606 if (value.isEmpty()) { 607 tileIndex = 0; 608 } else { 609 tileIndex = Integer.parseInt(value); 610 } 611 } catch (NumberFormatException e) { 612 return false; 613 } 614 if (tileIndex < 0 || tileIndex >= Math.pow(2, zoomLevel)) return false; 615 616 return true; 617 } 618 619 @Override 620 public void validate() { 621 if (isValid()) { 622 feedbackValid(tr("Please enter a tile index")); 623 } else { 624 feedbackInvalid(tr("The current value isn''t a valid tile index for the given zoom level", getComponent().getText())); 625 } 626 } 627 628 public int getTileIndex() { 629 return tileIndex; 630 } 631 } 632 633 /** 634 * Represents a rectangular area of tiles at a given zoom level. 635 */ 636 private static final class TileBounds { 637 private Point min; 638 private Point max; 639 private int zoomLevel; 640 641 private TileBounds() { 642 zoomLevel = 0; 643 min = new Point(0, 0); 644 max = new Point(0, 0); 645 } 646 647 private TileBounds(Point min, Point max, int zoomLevel) { 648 this.min = min; 649 this.max = max; 650 this.zoomLevel = zoomLevel; 651 } 652 653 @Override 654 public String toString() { 655 StringBuilder sb = new StringBuilder(24); 656 sb.append("min=").append(min.x).append(',').append(min.y) 657 .append(",max=").append(max.x).append(',').append(max.y) 658 .append(",zoom=").append(zoomLevel); 659 return sb.toString(); 660 } 661 } 662 663 /** 664 * The map view used in this bounding box chooser 665 */ 666 private static final class TileBoundsMapView extends JMapViewer { 667 private Point min; 668 private Point max; 669 670 private TileBoundsMapView() { 671 setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); 672 TileLoader loader = tileController.getTileLoader(); 673 if (loader instanceof OsmTileLoader) { 674 ((OsmTileLoader) loader).headers.put("User-Agent", Version.getInstance().getFullAgentString()); 675 } 676 } 677 678 public void setBoundingBox(Bounds bbox) { 679 if (bbox == null) { 680 min = null; 681 max = null; 682 } else { 683 Point p1 = tileSource.latLonToXY(bbox.getMinLat(), bbox.getMinLon(), MAX_ZOOM); 684 Point p2 = tileSource.latLonToXY(bbox.getMaxLat(), bbox.getMaxLon(), MAX_ZOOM); 685 686 min = new Point(Math.min(p1.x, p2.x), Math.min(p1.y, p2.y)); 687 max = new Point(Math.max(p1.x, p2.x), Math.max(p1.y, p2.y)); 688 } 689 repaint(); 690 } 691 692 protected Point getTopLeftCoordinates() { 693 return new Point(center.x - (getWidth() / 2), center.y - (getHeight() / 2)); 694 } 695 696 /** 697 * Draw the map. 698 */ 699 @Override 700 public void paint(Graphics g) { 701 try { 702 super.paint(g); 703 if (min == null || max == null) return; 704 int zoomDiff = MAX_ZOOM - zoom; 705 Point tlc = getTopLeftCoordinates(); 706 int x_min = (min.x >> zoomDiff) - tlc.x; 707 int y_min = (min.y >> zoomDiff) - tlc.y; 708 int x_max = (max.x >> zoomDiff) - tlc.x; 709 int y_max = (max.y >> zoomDiff) - tlc.y; 710 711 int w = x_max - x_min; 712 int h = y_max - y_min; 713 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f)); 714 g.fillRect(x_min, y_min, w, h); 715 716 g.setColor(Color.BLACK); 717 g.drawRect(x_min, y_min, w, h); 718 } catch (Exception e) { 719 Main.error(e); 720 } 721 } 722 } 723}