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