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