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