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