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