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