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} &lt;= zoom level &lt;= {@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} &lt;= zoom level &lt;=
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}