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