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