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