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