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