001// License: GPL. For details, see Readme.txt file. 002package org.openstreetmap.gui.jmapviewer; 003 004import java.awt.Dimension; 005import java.awt.Font; 006import java.awt.Graphics; 007import java.awt.Insets; 008import java.awt.Point; 009import java.awt.event.MouseEvent; 010import java.io.IOException; 011import java.net.URL; 012import java.util.ArrayList; 013import java.util.Collections; 014import java.util.List; 015 016import javax.swing.ImageIcon; 017import javax.swing.JButton; 018import javax.swing.JPanel; 019import javax.swing.JSlider; 020import javax.swing.event.EventListenerList; 021 022import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent; 023import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent.COMMAND; 024import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 025import org.openstreetmap.gui.jmapviewer.interfaces.JMapViewerEventListener; 026import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; 027import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon; 028import org.openstreetmap.gui.jmapviewer.interfaces.MapRectangle; 029import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 030import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 031import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 032import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 033import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource; 034 035/** 036 * Provides a simple panel that displays pre-rendered map tiles loaded from the 037 * OpenStreetMap project. 038 * 039 * @author Jan Peter Stotz 040 * @author Jason Huntley 041 */ 042public class JMapViewer extends JPanel implements TileLoaderListener { 043 044 private static final long serialVersionUID = 1L; 045 046 /** whether debug mode is enabled or not */ 047 public static boolean debug; 048 049 /** option to reverse zoom direction with mouse wheel */ 050 public static boolean zoomReverseWheel; 051 052 /** 053 * Vectors for clock-wise tile painting 054 */ 055 private static final Point[] move = {new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1)}; 056 057 /** Maximum zoom level */ 058 public static final int MAX_ZOOM = 24; 059 /** Minimum zoom level */ 060 public static final int MIN_ZOOM = 0; 061 062 protected transient List<MapMarker> mapMarkerList; 063 protected transient List<MapRectangle> mapRectangleList; 064 protected transient List<MapPolygon> mapPolygonList; 065 066 protected boolean mapMarkersVisible; 067 protected boolean mapRectanglesVisible; 068 protected boolean mapPolygonsVisible; 069 070 protected boolean tileGridVisible; 071 protected boolean scrollWrapEnabled; 072 073 protected transient TileController tileController; 074 075 /** 076 * x- and y-position of the center of this map-panel on the world map 077 * denoted in screen pixel regarding the current zoom level. 078 */ 079 protected Point center; 080 081 /** 082 * Current zoom level 083 */ 084 protected int zoom; 085 086 protected JSlider zoomSlider; 087 protected JButton zoomInButton; 088 protected JButton zoomOutButton; 089 090 /** 091 * Apparence of zoom controls. 092 */ 093 public enum ZOOM_BUTTON_STYLE { 094 /** Zoom buttons are displayed horizontally (default) */ 095 HORIZONTAL, 096 /** Zoom buttons are displayed vertically */ 097 VERTICAL 098 } 099 100 protected ZOOM_BUTTON_STYLE zoomButtonStyle; 101 102 protected transient TileSource tileSource; 103 104 protected transient AttributionSupport attribution = new AttributionSupport(); 105 106 protected EventListenerList evtListenerList = new EventListenerList(); 107 108 /** 109 * Creates a standard {@link JMapViewer} instance that can be controlled via 110 * mouse: hold right mouse button for moving, double click left mouse button 111 * or use mouse wheel for zooming. Loaded tiles are stored in a 112 * {@link MemoryTileCache} and the tile loader uses 4 parallel threads for 113 * retrieving the tiles. 114 */ 115 public JMapViewer() { 116 this(new MemoryTileCache()); 117 new DefaultMapController(this); 118 } 119 120 /** 121 * Creates a new {@link JMapViewer} instance. 122 * @param tileCache The cache where to store tiles 123 * @param downloadThreadCount not used anymore 124 * @deprecated use {@link #JMapViewer(TileCache)} 125 */ 126 @Deprecated 127 public JMapViewer(TileCache tileCache, int downloadThreadCount) { 128 this(tileCache); 129 } 130 131 /** 132 * Creates a new {@link JMapViewer} instance. 133 * @param tileCache The cache where to store tiles 134 * 135 */ 136 public JMapViewer(TileCache tileCache) { 137 tileSource = new OsmTileSource.Mapnik(); 138 tileController = new TileController(tileSource, tileCache, this); 139 mapMarkerList = Collections.synchronizedList(new ArrayList<MapMarker>()); 140 mapPolygonList = Collections.synchronizedList(new ArrayList<MapPolygon>()); 141 mapRectangleList = Collections.synchronizedList(new ArrayList<MapRectangle>()); 142 mapMarkersVisible = true; 143 mapRectanglesVisible = true; 144 mapPolygonsVisible = true; 145 tileGridVisible = false; 146 setLayout(null); 147 initializeZoomSlider(); 148 setMinimumSize(new Dimension(tileSource.getTileSize(), tileSource.getTileSize())); 149 setPreferredSize(new Dimension(400, 400)); 150 setDisplayPosition(new Coordinate(50, 9), 3); 151 } 152 153 @Override 154 public String getToolTipText(MouseEvent event) { 155 return super.getToolTipText(event); 156 } 157 158 protected void initializeZoomSlider() { 159 zoomSlider = new JSlider(MIN_ZOOM, tileController.getTileSource().getMaxZoom()); 160 zoomSlider.setOrientation(JSlider.VERTICAL); 161 zoomSlider.setBounds(10, 10, 30, 150); 162 zoomSlider.setOpaque(false); 163 zoomSlider.addChangeListener(e -> setZoom(zoomSlider.getValue())); 164 zoomSlider.setFocusable(false); 165 add(zoomSlider); 166 int size = 18; 167 ImageIcon icon = getImageIcon("images/plus.png"); 168 if (icon != null) { 169 zoomInButton = new JButton(icon); 170 } else { 171 zoomInButton = new JButton("+"); 172 zoomInButton.setFont(new Font("sansserif", Font.BOLD, 9)); 173 zoomInButton.setMargin(new Insets(0, 0, 0, 0)); 174 } 175 zoomInButton.setBounds(4, 155, size, size); 176 zoomInButton.addActionListener(e -> zoomIn()); 177 zoomInButton.setFocusable(false); 178 add(zoomInButton); 179 icon = getImageIcon("images/minus.png"); 180 if (icon != null) { 181 zoomOutButton = new JButton(icon); 182 } else { 183 zoomOutButton = new JButton("-"); 184 zoomOutButton.setFont(new Font("sansserif", Font.BOLD, 9)); 185 zoomOutButton.setMargin(new Insets(0, 0, 0, 0)); 186 } 187 zoomOutButton.setBounds(8 + size, 155, size, size); 188 zoomOutButton.addActionListener(e -> zoomOut()); 189 zoomOutButton.setFocusable(false); 190 add(zoomOutButton); 191 } 192 193 private static ImageIcon getImageIcon(String name) { 194 URL url = JMapViewer.class.getResource(name); 195 if (url != null) { 196 try { 197 return new ImageIcon(FeatureAdapter.readImage(url)); 198 } catch (IOException e) { 199 e.printStackTrace(); 200 } 201 } 202 return null; 203 } 204 205 /** 206 * Changes the map pane so that it is centered on the specified coordinate 207 * at the given zoom level. 208 * 209 * @param to 210 * specified coordinate 211 * @param zoom 212 * {@link #MIN_ZOOM} <= zoom level <= {@link #MAX_ZOOM} 213 */ 214 public void setDisplayPosition(ICoordinate to, int zoom) { 215 setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), to, zoom); 216 } 217 218 /** 219 * Changes the map pane so that the specified coordinate at the given zoom 220 * level is displayed on the map at the screen coordinate 221 * <code>mapPoint</code>. 222 * 223 * @param mapPoint 224 * point on the map denoted in pixels where the coordinate should 225 * be set 226 * @param to 227 * specified coordinate 228 * @param zoom 229 * {@link #MIN_ZOOM} <= zoom level <= 230 * {@link TileSource#getMaxZoom()} 231 */ 232 public void setDisplayPosition(Point mapPoint, ICoordinate to, int zoom) { 233 Point p = tileSource.latLonToXY(to, zoom); 234 setDisplayPosition(mapPoint, p.x, p.y, zoom); 235 } 236 237 /** 238 * Sets the display position. 239 * @param x X coordinate 240 * @param y Y coordinate 241 * @param zoom zoom level, between {@link #MIN_ZOOM} and {@link #MAX_ZOOM} 242 */ 243 public void setDisplayPosition(int x, int y, int zoom) { 244 setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), x, y, zoom); 245 } 246 247 /** 248 * Sets the display position. 249 * @param mapPoint map point 250 * @param x X coordinate 251 * @param y Y coordinate 252 * @param zoom zoom level, between {@link #MIN_ZOOM} and {@link #MAX_ZOOM} 253 */ 254 public void setDisplayPosition(Point mapPoint, int x, int y, int zoom) { 255 if (zoom > tileController.getTileSource().getMaxZoom() || zoom < MIN_ZOOM) 256 return; 257 258 // Get the plain tile number 259 Point p = new Point(); 260 p.x = x - mapPoint.x + getWidth() / 2; 261 p.y = y - mapPoint.y + getHeight() / 2; 262 center = p; 263 setIgnoreRepaint(true); 264 try { 265 int oldZoom = this.zoom; 266 this.zoom = zoom; 267 if (oldZoom != zoom) { 268 zoomChanged(oldZoom); 269 } 270 if (zoomSlider.getValue() != zoom) { 271 zoomSlider.setValue(zoom); 272 } 273 } finally { 274 setIgnoreRepaint(false); 275 repaint(); 276 } 277 } 278 279 /** 280 * Sets the displayed map pane and zoom level so that all chosen map elements are visible. 281 * @param markers whether to consider markers 282 * @param rectangles whether to consider rectangles 283 * @param polygons whether to consider polygons 284 */ 285 public void setDisplayToFitMapElements(boolean markers, boolean rectangles, boolean polygons) { 286 int nbElemToCheck = 0; 287 if (markers && mapMarkerList != null) 288 nbElemToCheck += mapMarkerList.size(); 289 if (rectangles && mapRectangleList != null) 290 nbElemToCheck += mapRectangleList.size(); 291 if (polygons && mapPolygonList != null) 292 nbElemToCheck += mapPolygonList.size(); 293 if (nbElemToCheck == 0) 294 return; 295 296 int xMin = Integer.MAX_VALUE; 297 int yMin = Integer.MAX_VALUE; 298 int xMax = Integer.MIN_VALUE; 299 int yMax = Integer.MIN_VALUE; 300 /* 301 * Cap mapZoomMax at highest level that prevents overflowing int in X and Y coordinates. As int is from -2^31..2^31. 302 * Log_2(TileSize) is how many bits are used due to tile size. Math.log(TileSize) / Math.log(2) gives Log_2(TileSize) 303 * So 31 - tileSizeBits gives maximum zoom that can be handled without overflowing. 304 * It means 23 for 256 tile size or 22 for 512 tile size 305 */ 306 int tileSizeBits = (int) (Math.log(tileController.getTileSource().getDefaultTileSize()) / Math.log(2)); 307 int mapZoomMax = Math.min(31 - tileSizeBits, tileController.getTileSource().getMaxZoom()); 308 309 if (markers && mapMarkerList != null) { 310 synchronized (this) { 311 for (MapMarker marker : mapMarkerList) { 312 if (marker.isVisible()) { 313 Point p = tileSource.latLonToXY(marker.getCoordinate(), mapZoomMax); 314 xMax = Math.max(xMax, p.x); 315 yMax = Math.max(yMax, p.y); 316 xMin = Math.min(xMin, p.x); 317 yMin = Math.min(yMin, p.y); 318 } 319 } 320 } 321 } 322 323 if (rectangles && mapRectangleList != null) { 324 synchronized (this) { 325 for (MapRectangle rectangle : mapRectangleList) { 326 if (rectangle.isVisible()) { 327 Point bottomRight = tileSource.latLonToXY(rectangle.getBottomRight(), mapZoomMax); 328 Point topLeft = tileSource.latLonToXY(rectangle.getTopLeft(), mapZoomMax); 329 xMax = Math.max(xMax, bottomRight.x); 330 yMax = Math.max(yMax, topLeft.y); 331 xMin = Math.min(xMin, topLeft.x); 332 yMin = Math.min(yMin, bottomRight.y); 333 } 334 } 335 } 336 } 337 338 if (polygons && mapPolygonList != null) { 339 synchronized (this) { 340 for (MapPolygon polygon : mapPolygonList) { 341 if (polygon.isVisible()) { 342 for (ICoordinate c : polygon.getPoints()) { 343 Point p = tileSource.latLonToXY(c, mapZoomMax); 344 xMax = Math.max(xMax, p.x); 345 yMax = Math.max(yMax, p.y); 346 xMin = Math.min(xMin, p.x); 347 yMin = Math.min(yMin, p.y); 348 } 349 } 350 } 351 } 352 } 353 354 int height = Math.max(0, getHeight()); 355 int width = Math.max(0, getWidth()); 356 int newZoom = mapZoomMax; 357 int x = xMax - xMin; 358 int y = yMax - yMin; 359 while (x > width || y > height) { 360 newZoom--; 361 x >>= 1; 362 y >>= 1; 363 } 364 x = xMin + (xMax - xMin) / 2; 365 y = yMin + (yMax - yMin) / 2; 366 int z = 1 << (mapZoomMax - newZoom); 367 x /= z; 368 y /= z; 369 setDisplayPosition(x, y, newZoom); 370 } 371 372 /** 373 * Sets the displayed map pane and zoom level so that all map markers are visible. 374 */ 375 public void setDisplayToFitMapMarkers() { 376 setDisplayToFitMapElements(true, false, false); 377 } 378 379 /** 380 * Sets the displayed map pane and zoom level so that all map rectangles are visible. 381 */ 382 public void setDisplayToFitMapRectangles() { 383 setDisplayToFitMapElements(false, true, false); 384 } 385 386 /** 387 * Sets the displayed map pane and zoom level so that all map polygons are visible. 388 */ 389 public void setDisplayToFitMapPolygons() { 390 setDisplayToFitMapElements(false, false, true); 391 } 392 393 /** 394 * @return the center 395 */ 396 public Point getCenter() { 397 return center; 398 } 399 400 /** 401 * @param center the center to set 402 */ 403 public void setCenter(Point center) { 404 this.center = center; 405 } 406 407 /** 408 * Calculates the latitude/longitude coordinate of the center of the 409 * currently displayed map area. 410 * 411 * @return latitude / longitude 412 */ 413 public ICoordinate getPosition() { 414 return tileSource.xyToLatLon(center, zoom); 415 } 416 417 /** 418 * Converts the relative pixel coordinate (regarding the top left corner of 419 * the displayed map) into a latitude / longitude coordinate 420 * 421 * @param mapPoint 422 * relative pixel coordinate regarding the top left corner of the 423 * displayed map 424 * @return latitude / longitude 425 */ 426 public ICoordinate getPosition(Point mapPoint) { 427 return getPosition(mapPoint.x, mapPoint.y); 428 } 429 430 /** 431 * Converts the relative pixel coordinate (regarding the top left corner of 432 * the displayed map) into a latitude / longitude coordinate 433 * 434 * @param mapPointX X coordinate 435 * @param mapPointY Y coordinate 436 * @return latitude / longitude 437 */ 438 public ICoordinate getPosition(int mapPointX, int mapPointY) { 439 int x = center.x + mapPointX - getWidth() / 2; 440 int y = center.y + mapPointY - getHeight() / 2; 441 return tileSource.xyToLatLon(x, y, zoom); 442 } 443 444 /** 445 * Calculates the position on the map of a given coordinate 446 * 447 * @param lat latitude 448 * @param lon longitude 449 * @param checkOutside check if the point is outside the displayed area 450 * @return point on the map or <code>null</code> if the point is not visible 451 * and checkOutside set to <code>true</code> 452 */ 453 public Point getMapPosition(double lat, double lon, boolean checkOutside) { 454 Point p = tileSource.latLonToXY(lat, lon, zoom); 455 p.translate(-(center.x - getWidth() / 2), -(center.y - getHeight() /2)); 456 457 if (checkOutside && (p.x < 0 || p.y < 0 || p.x > getWidth() || p.y > getHeight())) { 458 return null; 459 } 460 return p; 461 } 462 463 /** 464 * Calculates the position on the map of a given coordinate 465 * 466 * @param lat latitude 467 * @param lon longitude 468 * @return point on the map or <code>null</code> if the point is not visible 469 */ 470 public Point getMapPosition(double lat, double lon) { 471 return getMapPosition(lat, lon, true); 472 } 473 474 /** 475 * Calculates the position on the map of a given coordinate 476 * 477 * @param lat Latitude 478 * @param lon longitude 479 * @param offset Offset respect Latitude 480 * @param checkOutside check if the point is outside the displayed area 481 * @return Integer the radius in pixels 482 */ 483 public Integer getLatOffset(double lat, double lon, double offset, boolean checkOutside) { 484 Point p = tileSource.latLonToXY(lat + offset, lon, zoom); 485 int y = p.y - (center.y - getHeight() / 2); 486 if (checkOutside && (y < 0 || y > getHeight())) { 487 return null; 488 } 489 return y; 490 } 491 492 /** 493 * Calculates the position on the map of a given coordinate 494 * 495 * @param marker MapMarker object that define the x,y coordinate 496 * @param p coordinate 497 * @return Integer the radius in pixels 498 */ 499 public Integer getRadius(MapMarker marker, Point p) { 500 if (marker.getMarkerStyle() == MapMarker.STYLE.FIXED) 501 return (int) marker.getRadius(); 502 else if (p != null) { 503 Integer radius = getLatOffset(marker.getLat(), marker.getLon(), marker.getRadius(), false); 504 radius = radius == null ? null : p.y - radius; 505 return radius; 506 } else 507 return null; 508 } 509 510 /** 511 * Calculates the position on the map of a given coordinate 512 * 513 * @param coord coordinate 514 * @return point on the map or <code>null</code> if the point is not visible 515 */ 516 public Point getMapPosition(Coordinate coord) { 517 if (coord != null) 518 return getMapPosition(coord.getLat(), coord.getLon()); 519 else 520 return null; 521 } 522 523 /** 524 * Calculates the position on the map of a given coordinate 525 * 526 * @param coord coordinate 527 * @param checkOutside check if the point is outside the displayed area 528 * @return point on the map or <code>null</code> if the point is not visible 529 * and checkOutside set to <code>true</code> 530 */ 531 public Point getMapPosition(ICoordinate coord, boolean checkOutside) { 532 if (coord != null) 533 return getMapPosition(coord.getLat(), coord.getLon(), checkOutside); 534 else 535 return null; 536 } 537 538 /** 539 * Gets the meter per pixel. 540 * 541 * @return the meter per pixel 542 */ 543 public double getMeterPerPixel() { 544 Point origin = new Point(5, 5); 545 Point center = new Point(getWidth() / 2, getHeight() / 2); 546 547 double pDistance = center.distance(origin); 548 549 ICoordinate originCoord = getPosition(origin); 550 ICoordinate centerCoord = getPosition(center); 551 552 double mDistance = tileSource.getDistance(originCoord.getLat(), originCoord.getLon(), 553 centerCoord.getLat(), centerCoord.getLon()); 554 555 return mDistance / pDistance; 556 } 557 558 @Override 559 protected void paintComponent(Graphics g) { 560 super.paintComponent(g); 561 562 int iMove = 0; 563 564 int tilesize = tileSource.getTileSize(); 565 int tilex = center.x / tilesize; 566 int tiley = center.y / tilesize; 567 int offsx = center.x % tilesize; 568 int offsy = center.y % tilesize; 569 570 int w2 = getWidth() / 2; 571 int h2 = getHeight() / 2; 572 int posx = w2 - offsx; 573 int posy = h2 - offsy; 574 575 int diffLeft = offsx; 576 int diffRight = tilesize - offsx; 577 int diffTop = offsy; 578 int diffBottom = tilesize - offsy; 579 580 boolean startLeft = diffLeft < diffRight; 581 boolean startTop = diffTop < diffBottom; 582 583 if (startTop) { 584 if (startLeft) { 585 iMove = 2; 586 } else { 587 iMove = 3; 588 } 589 } else { 590 if (startLeft) { 591 iMove = 1; 592 } else { 593 iMove = 0; 594 } 595 } // calculate the visibility borders 596 int xMin = -tilesize; 597 int yMin = -tilesize; 598 int xMax = getWidth(); 599 int yMax = getHeight(); 600 601 // calculate the length of the grid (number of squares per edge) 602 int gridLength = 1 << zoom; 603 604 // paint the tiles in a spiral, starting from center of the map 605 boolean painted = true; 606 int x = 0; 607 while (painted) { 608 painted = false; 609 for (int i = 0; i < 4; i++) { 610 if (i % 2 == 0) { 611 x++; 612 } 613 for (int j = 0; j < x; j++) { 614 if (xMin <= posx && posx <= xMax && yMin <= posy && posy <= yMax) { 615 // tile is visible 616 Tile tile; 617 if (scrollWrapEnabled) { 618 // in case tilex is out of bounds, grab the tile to use for wrapping 619 int tilexWrap = ((tilex % gridLength) + gridLength) % gridLength; 620 tile = tileController.getTile(tilexWrap, tiley, zoom); 621 } else { 622 tile = tileController.getTile(tilex, tiley, zoom); 623 } 624 if (tile != null) { 625 tile.paint(g, posx, posy, tilesize, tilesize); 626 if (tileGridVisible) { 627 g.drawRect(posx, posy, tilesize, tilesize); 628 } 629 } 630 painted = true; 631 } 632 Point p = move[iMove]; 633 posx += p.x * tilesize; 634 posy += p.y * tilesize; 635 tilex += p.x; 636 tiley += p.y; 637 } 638 iMove = (iMove + 1) % move.length; 639 } 640 } 641 // outer border of the map 642 int mapSize = tilesize << zoom; 643 if (scrollWrapEnabled) { 644 g.drawLine(0, h2 - center.y, getWidth(), h2 - center.y); 645 g.drawLine(0, h2 - center.y + mapSize, getWidth(), h2 - center.y + mapSize); 646 } else { 647 g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize); 648 } 649 650 // g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20); 651 652 // keep x-coordinates from growing without bound if scroll-wrap is enabled 653 if (scrollWrapEnabled) { 654 center.x = center.x % mapSize; 655 } 656 657 if (mapPolygonsVisible && mapPolygonList != null) { 658 synchronized (this) { 659 for (MapPolygon polygon : mapPolygonList) { 660 if (polygon.isVisible()) 661 paintPolygon(g, polygon); 662 } 663 } 664 } 665 666 if (mapRectanglesVisible && mapRectangleList != null) { 667 synchronized (this) { 668 for (MapRectangle rectangle : mapRectangleList) { 669 if (rectangle.isVisible()) 670 paintRectangle(g, rectangle); 671 } 672 } 673 } 674 675 if (mapMarkersVisible && mapMarkerList != null) { 676 synchronized (this) { 677 for (MapMarker marker : mapMarkerList) { 678 if (marker.isVisible()) 679 paintMarker(g, marker); 680 } 681 } 682 } 683 684 attribution.paintAttribution(g, getWidth(), getHeight(), getPosition(0, 0), getPosition(getWidth(), getHeight()), zoom, this); 685 } 686 687 /** 688 * Paint a single marker. 689 * @param g Graphics used for painting 690 * @param marker marker to paint 691 */ 692 protected void paintMarker(Graphics g, MapMarker marker) { 693 Point p = getMapPosition(marker.getLat(), marker.getLon(), marker.getMarkerStyle() == MapMarker.STYLE.FIXED); 694 Integer radius = getRadius(marker, p); 695 if (scrollWrapEnabled) { 696 int tilesize = tileSource.getTileSize(); 697 int mapSize = tilesize << zoom; 698 if (p == null) { 699 p = getMapPosition(marker.getLat(), marker.getLon(), false); 700 radius = getRadius(marker, p); 701 } 702 marker.paint(g, p, radius); 703 int xSave = p.x; 704 int xWrap = xSave; 705 // overscan of 15 allows up to 30-pixel markers to gracefully scroll off the edge of the panel 706 while ((xWrap -= mapSize) >= -15) { 707 p.x = xWrap; 708 marker.paint(g, p, radius); 709 } 710 xWrap = xSave; 711 while ((xWrap += mapSize) <= getWidth() + 15) { 712 p.x = xWrap; 713 marker.paint(g, p, radius); 714 } 715 } else { 716 if (p != null) { 717 marker.paint(g, p, radius); 718 } 719 } 720 } 721 722 /** 723 * Paint a single rectangle. 724 * @param g Graphics used for painting 725 * @param rectangle rectangle to paint 726 */ 727 protected void paintRectangle(Graphics g, MapRectangle rectangle) { 728 Coordinate topLeft = rectangle.getTopLeft(); 729 Coordinate bottomRight = rectangle.getBottomRight(); 730 if (topLeft != null && bottomRight != null) { 731 Point pTopLeft = getMapPosition(topLeft, false); 732 Point pBottomRight = getMapPosition(bottomRight, false); 733 if (pTopLeft != null && pBottomRight != null) { 734 rectangle.paint(g, pTopLeft, pBottomRight); 735 if (scrollWrapEnabled) { 736 int tilesize = tileSource.getTileSize(); 737 int mapSize = tilesize << zoom; 738 int xTopLeftSave = pTopLeft.x; 739 int xTopLeftWrap = xTopLeftSave; 740 int xBottomRightSave = pBottomRight.x; 741 int xBottomRightWrap = xBottomRightSave; 742 while ((xBottomRightWrap -= mapSize) >= 0) { 743 xTopLeftWrap -= mapSize; 744 pTopLeft.x = xTopLeftWrap; 745 pBottomRight.x = xBottomRightWrap; 746 rectangle.paint(g, pTopLeft, pBottomRight); 747 } 748 xTopLeftWrap = xTopLeftSave; 749 xBottomRightWrap = xBottomRightSave; 750 while ((xTopLeftWrap += mapSize) <= getWidth()) { 751 xBottomRightWrap += mapSize; 752 pTopLeft.x = xTopLeftWrap; 753 pBottomRight.x = xBottomRightWrap; 754 rectangle.paint(g, pTopLeft, pBottomRight); 755 } 756 } 757 } 758 } 759 } 760 761 /** 762 * Paint a single polygon. 763 * @param g Graphics used for painting 764 * @param polygon polygon to paint 765 */ 766 protected void paintPolygon(Graphics g, MapPolygon polygon) { 767 List<? extends ICoordinate> coords = polygon.getPoints(); 768 if (coords != null && coords.size() >= 3) { 769 List<Point> points = new ArrayList<>(); 770 for (ICoordinate c : coords) { 771 Point p = getMapPosition(c, false); 772 if (p == null) { 773 return; 774 } 775 points.add(p); 776 } 777 polygon.paint(g, points); 778 if (scrollWrapEnabled) { 779 int tilesize = tileSource.getTileSize(); 780 int mapSize = tilesize << zoom; 781 List<Point> pointsWrapped = new ArrayList<>(points); 782 boolean keepWrapping = true; 783 while (keepWrapping) { 784 for (Point p : pointsWrapped) { 785 p.x -= mapSize; 786 if (p.x < 0) { 787 keepWrapping = false; 788 } 789 } 790 polygon.paint(g, pointsWrapped); 791 } 792 pointsWrapped = new ArrayList<>(points); 793 keepWrapping = true; 794 while (keepWrapping) { 795 for (Point p : pointsWrapped) { 796 p.x += mapSize; 797 if (p.x > getWidth()) { 798 keepWrapping = false; 799 } 800 } 801 polygon.paint(g, pointsWrapped); 802 } 803 } 804 } 805 } 806 807 /** 808 * Moves the visible map pane. 809 * 810 * @param x 811 * horizontal movement in pixel. 812 * @param y 813 * vertical movement in pixel 814 */ 815 public void moveMap(int x, int y) { 816 tileController.cancelOutstandingJobs(); // Clear outstanding load 817 center.x += x; 818 center.y += y; 819 repaint(); 820 this.fireJMVEvent(new JMVCommandEvent(COMMAND.MOVE, this)); 821 } 822 823 /** 824 * @return the current zoom level 825 */ 826 public int getZoom() { 827 return zoom; 828 } 829 830 /** 831 * Increases the current zoom level by one 832 */ 833 public void zoomIn() { 834 setZoom(zoom + 1); 835 } 836 837 /** 838 * Increases the current zoom level by one 839 * @param mapPoint point to choose as center for new zoom level 840 */ 841 public void zoomIn(Point mapPoint) { 842 setZoom(zoom + 1, mapPoint); 843 } 844 845 /** 846 * Decreases the current zoom level by one 847 */ 848 public void zoomOut() { 849 setZoom(zoom - 1); 850 } 851 852 /** 853 * Decreases the current zoom level by one 854 * 855 * @param mapPoint point to choose as center for new zoom level 856 */ 857 public void zoomOut(Point mapPoint) { 858 setZoom(zoom - 1, mapPoint); 859 } 860 861 /** 862 * Set the zoom level and center point for display 863 * 864 * @param zoom new zoom level 865 * @param mapPoint point to choose as center for new zoom level 866 */ 867 public void setZoom(int zoom, Point mapPoint) { 868 if (zoom > tileController.getTileSource().getMaxZoom() || zoom < tileController.getTileSource().getMinZoom() 869 || zoom == this.zoom) 870 return; 871 ICoordinate zoomPos = getPosition(mapPoint); 872 tileController.cancelOutstandingJobs(); // Clearing outstanding load 873 // requests 874 setDisplayPosition(mapPoint, zoomPos, zoom); 875 876 this.fireJMVEvent(new JMVCommandEvent(COMMAND.ZOOM, this)); 877 } 878 879 /** 880 * Set the zoom level 881 * 882 * @param zoom new zoom level 883 */ 884 public void setZoom(int zoom) { 885 setZoom(zoom, new Point(getWidth() / 2, getHeight() / 2)); 886 } 887 888 /** 889 * Every time the zoom level changes this method is called. Override it in 890 * derived implementations for adapting zoom dependent values. The new zoom 891 * level can be obtained via {@link #getZoom()}. 892 * 893 * @param oldZoom the previous zoom level 894 */ 895 protected void zoomChanged(int oldZoom) { 896 zoomSlider.setToolTipText("Zoom level " + zoom); 897 zoomInButton.setToolTipText("Zoom to level " + (zoom + 1)); 898 zoomOutButton.setToolTipText("Zoom to level " + (zoom - 1)); 899 zoomOutButton.setEnabled(zoom > tileController.getTileSource().getMinZoom()); 900 zoomInButton.setEnabled(zoom < tileController.getTileSource().getMaxZoom()); 901 } 902 903 /** 904 * Determines whether the tile grid is visible or not. 905 * @return {@code true} if the tile grid is visible, {@code false} otherwise 906 */ 907 public boolean isTileGridVisible() { 908 return tileGridVisible; 909 } 910 911 /** 912 * Sets whether the tile grid is visible or not. 913 * @param tileGridVisible {@code true} if the tile grid is visible, {@code false} otherwise 914 */ 915 public void setTileGridVisible(boolean tileGridVisible) { 916 this.tileGridVisible = tileGridVisible; 917 repaint(); 918 } 919 920 /** 921 * Determines whether {@link MapMarker}s are painted or not. 922 * @return {@code true} if {@link MapMarker}s are painted, {@code false} otherwise 923 */ 924 public boolean getMapMarkersVisible() { 925 return mapMarkersVisible; 926 } 927 928 /** 929 * Enables or disables painting of the {@link MapMarker} 930 * 931 * @param mapMarkersVisible {@code true} to enable painting of markers 932 * @see #addMapMarker(MapMarker) 933 * @see #getMapMarkerList() 934 */ 935 public void setMapMarkerVisible(boolean mapMarkersVisible) { 936 this.mapMarkersVisible = mapMarkersVisible; 937 repaint(); 938 } 939 940 /** 941 * Sets the list of {@link MapMarker}s. 942 * @param mapMarkerList list of {@link MapMarker}s 943 */ 944 public void setMapMarkerList(List<MapMarker> mapMarkerList) { 945 this.mapMarkerList = mapMarkerList; 946 repaint(); 947 } 948 949 /** 950 * Returns the list of {@link MapMarker}s. 951 * @return list of {@link MapMarker}s 952 */ 953 public List<MapMarker> getMapMarkerList() { 954 return mapMarkerList; 955 } 956 957 /** 958 * Sets the list of {@link MapRectangle}s. 959 * @param mapRectangleList list of {@link MapRectangle}s 960 */ 961 public void setMapRectangleList(List<MapRectangle> mapRectangleList) { 962 this.mapRectangleList = mapRectangleList; 963 repaint(); 964 } 965 966 /** 967 * Returns the list of {@link MapRectangle}s. 968 * @return list of {@link MapRectangle}s 969 */ 970 public List<MapRectangle> getMapRectangleList() { 971 return mapRectangleList; 972 } 973 974 /** 975 * Sets the list of {@link MapPolygon}s. 976 * @param mapPolygonList list of {@link MapPolygon}s 977 */ 978 public void setMapPolygonList(List<MapPolygon> mapPolygonList) { 979 this.mapPolygonList = mapPolygonList; 980 repaint(); 981 } 982 983 /** 984 * Returns the list of {@link MapPolygon}s. 985 * @return list of {@link MapPolygon}s 986 */ 987 public List<MapPolygon> getMapPolygonList() { 988 return mapPolygonList; 989 } 990 991 /** 992 * Add a {@link MapMarker}. 993 * @param marker map marker to add 994 */ 995 public void addMapMarker(MapMarker marker) { 996 mapMarkerList.add(marker); 997 repaint(); 998 } 999 1000 /** 1001 * Remove a {@link MapMarker}. 1002 * @param marker map marker to remove 1003 */ 1004 public void removeMapMarker(MapMarker marker) { 1005 mapMarkerList.remove(marker); 1006 repaint(); 1007 } 1008 1009 /** 1010 * Remove all {@link MapMarker}s. 1011 */ 1012 public void removeAllMapMarkers() { 1013 mapMarkerList.clear(); 1014 repaint(); 1015 } 1016 1017 /** 1018 * Add a {@link MapRectangle}. 1019 * @param rectangle map rectangle to add 1020 */ 1021 public void addMapRectangle(MapRectangle rectangle) { 1022 mapRectangleList.add(rectangle); 1023 repaint(); 1024 } 1025 1026 /** 1027 * Remove a {@link MapRectangle}. 1028 * @param rectangle map rectangle to remove 1029 */ 1030 public void removeMapRectangle(MapRectangle rectangle) { 1031 mapRectangleList.remove(rectangle); 1032 repaint(); 1033 } 1034 1035 /** 1036 * Remove all {@link MapRectangle}s. 1037 */ 1038 public void removeAllMapRectangles() { 1039 mapRectangleList.clear(); 1040 repaint(); 1041 } 1042 1043 /** 1044 * Add a {@link MapPolygon}. 1045 * @param polygon map polygon to add 1046 */ 1047 public void addMapPolygon(MapPolygon polygon) { 1048 mapPolygonList.add(polygon); 1049 repaint(); 1050 } 1051 1052 /** 1053 * Remove a {@link MapPolygon}. 1054 * @param polygon map polygon to remove 1055 */ 1056 public void removeMapPolygon(MapPolygon polygon) { 1057 mapPolygonList.remove(polygon); 1058 repaint(); 1059 } 1060 1061 /** 1062 * Remove all {@link MapPolygon}s. 1063 */ 1064 public void removeAllMapPolygons() { 1065 mapPolygonList.clear(); 1066 repaint(); 1067 } 1068 1069 /** 1070 * Sets whether zoom controls are displayed or not. 1071 * @param visible {@code true} if zoom controls are displayed, {@code false} otherwise 1072 * @deprecated use {@link #setZoomControlsVisible(boolean)} 1073 */ 1074 @Deprecated 1075 public void setZoomContolsVisible(boolean visible) { 1076 setZoomControlsVisible(visible); 1077 } 1078 1079 /** 1080 * Sets whether zoom controls are displayed or not. 1081 * @param visible {@code true} if zoom controls are displayed, {@code false} otherwise 1082 */ 1083 public void setZoomControlsVisible(boolean visible) { 1084 zoomSlider.setVisible(visible); 1085 zoomInButton.setVisible(visible); 1086 zoomOutButton.setVisible(visible); 1087 } 1088 1089 /** 1090 * Determines whether zoom controls are displayed or not. 1091 * @return {@code true} if zoom controls are displayed, {@code false} otherwise 1092 */ 1093 public boolean getZoomControlsVisible() { 1094 return zoomSlider.isVisible(); 1095 } 1096 1097 /** 1098 * Sets the tile source. 1099 * @param tileSource tile source 1100 */ 1101 public void setTileSource(TileSource tileSource) { 1102 if (tileSource.getMaxZoom() > MAX_ZOOM) 1103 throw new RuntimeException("Maximum zoom level too high"); 1104 if (tileSource.getMinZoom() < MIN_ZOOM) 1105 throw new RuntimeException("Minimum zoom level too low"); 1106 ICoordinate position = getPosition(); 1107 this.tileSource = tileSource; 1108 tileController.setTileSource(tileSource); 1109 zoomSlider.setMinimum(tileSource.getMinZoom()); 1110 zoomSlider.setMaximum(tileSource.getMaxZoom()); 1111 tileController.cancelOutstandingJobs(); 1112 if (zoom > tileSource.getMaxZoom()) { 1113 setZoom(tileSource.getMaxZoom()); 1114 } 1115 attribution.initialize(tileSource); 1116 setDisplayPosition(position, zoom); 1117 repaint(); 1118 } 1119 1120 @Override 1121 public void tileLoadingFinished(Tile tile, boolean success) { 1122 tile.setLoaded(success); 1123 repaint(); 1124 } 1125 1126 /** 1127 * Determines whether the {@link MapRectangle}s are painted or not. 1128 * @return {@code true} if the {@link MapRectangle}s are painted, {@code false} otherwise 1129 */ 1130 public boolean isMapRectanglesVisible() { 1131 return mapRectanglesVisible; 1132 } 1133 1134 /** 1135 * Enables or disables painting of the {@link MapRectangle}s. 1136 * 1137 * @param mapRectanglesVisible {@code true} to enable painting of rectangles 1138 * @see #addMapRectangle(MapRectangle) 1139 * @see #getMapRectangleList() 1140 */ 1141 public void setMapRectanglesVisible(boolean mapRectanglesVisible) { 1142 this.mapRectanglesVisible = mapRectanglesVisible; 1143 repaint(); 1144 } 1145 1146 /** 1147 * Determines whether the {@link MapPolygon}s are painted or not. 1148 * @return {@code true} if the {@link MapPolygon}s are painted, {@code false} otherwise 1149 */ 1150 public boolean isMapPolygonsVisible() { 1151 return mapPolygonsVisible; 1152 } 1153 1154 /** 1155 * Enables or disables painting of the {@link MapPolygon}s. 1156 * 1157 * @param mapPolygonsVisible {@code true} to enable painting of polygons 1158 * @see #addMapPolygon(MapPolygon) 1159 * @see #getMapPolygonList() 1160 */ 1161 public void setMapPolygonsVisible(boolean mapPolygonsVisible) { 1162 this.mapPolygonsVisible = mapPolygonsVisible; 1163 repaint(); 1164 } 1165 1166 /** 1167 * Determines whether scroll wrap is enabled or not. 1168 * @return {@code true} if scroll wrap is enabled, {@code false} otherwise 1169 */ 1170 public boolean isScrollWrapEnabled() { 1171 return scrollWrapEnabled; 1172 } 1173 1174 /** 1175 * Sets whether scroll wrap is enabled or not. 1176 * @param scrollWrapEnabled {@code true} if scroll wrap is enabled, {@code false} otherwise 1177 */ 1178 public void setScrollWrapEnabled(boolean scrollWrapEnabled) { 1179 this.scrollWrapEnabled = scrollWrapEnabled; 1180 repaint(); 1181 } 1182 1183 /** 1184 * Returns the zoom controls apparence style (horizontal/vertical). 1185 * @return {@link ZOOM_BUTTON_STYLE#VERTICAL} or {@link ZOOM_BUTTON_STYLE#HORIZONTAL} 1186 */ 1187 public ZOOM_BUTTON_STYLE getZoomButtonStyle() { 1188 return zoomButtonStyle; 1189 } 1190 1191 /** 1192 * Sets the zoom controls apparence style (horizontal/vertical). 1193 * @param style {@link ZOOM_BUTTON_STYLE#VERTICAL} or {@link ZOOM_BUTTON_STYLE#HORIZONTAL} 1194 */ 1195 public void setZoomButtonStyle(ZOOM_BUTTON_STYLE style) { 1196 zoomButtonStyle = style; 1197 if (zoomSlider == null || zoomInButton == null || zoomOutButton == null) { 1198 return; 1199 } 1200 switch (style) { 1201 case VERTICAL: 1202 zoomSlider.setBounds(10, 27, 30, 150); 1203 zoomInButton.setBounds(14, 8, 20, 20); 1204 zoomOutButton.setBounds(14, 176, 20, 20); 1205 break; 1206 case HORIZONTAL: 1207 default: 1208 zoomSlider.setBounds(10, 10, 30, 150); 1209 zoomInButton.setBounds(4, 155, 18, 18); 1210 zoomOutButton.setBounds(26, 155, 18, 18); 1211 break; 1212 } 1213 repaint(); 1214 } 1215 1216 /** 1217 * Returns the tile controller. 1218 * @return the tile controller 1219 */ 1220 public TileController getTileController() { 1221 return tileController; 1222 } 1223 1224 /** 1225 * Return tile information caching class 1226 * @return tile cache 1227 * @see TileController#getTileCache() 1228 */ 1229 public TileCache getTileCache() { 1230 return tileController.getTileCache(); 1231 } 1232 1233 /** 1234 * Sets the tile loader. 1235 * @param loader tile loader 1236 */ 1237 public void setTileLoader(TileLoader loader) { 1238 tileController.setTileLoader(loader); 1239 } 1240 1241 /** 1242 * Returns attribution. 1243 * @return attribution 1244 */ 1245 public AttributionSupport getAttribution() { 1246 return attribution; 1247 } 1248 1249 /** 1250 * @param listener listener to set 1251 */ 1252 public void addJMVListener(JMapViewerEventListener listener) { 1253 evtListenerList.add(JMapViewerEventListener.class, listener); 1254 } 1255 1256 /** 1257 * @param listener listener to remove 1258 */ 1259 public void removeJMVListener(JMapViewerEventListener listener) { 1260 evtListenerList.remove(JMapViewerEventListener.class, listener); 1261 } 1262 1263 /** 1264 * Send an update to all objects registered with viewer 1265 * 1266 * @param evt event to dispatch 1267 */ 1268 private void fireJMVEvent(JMVCommandEvent evt) { 1269 Object[] listeners = evtListenerList.getListenerList(); 1270 for (int i = 0; i < listeners.length; i += 2) { 1271 if (listeners[i] == JMapViewerEventListener.class) { 1272 ((JMapViewerEventListener) listeners[i + 1]).processCommand(evt); 1273 } 1274 } 1275 } 1276}