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