package org.openstreetmap.gui.jmapviewer;
//License: GPL. Copyright 2008 by Jan Peter Stotz
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.LinkedList;
import java.util.List;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
import org.openstreetmap.gui.jmapviewer.interfaces.MapRectangle;
import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
/**
*
* Provides a simple panel that displays pre-rendered map tiles loaded from the
* OpenStreetMap project.
*
* @author Jan Peter Stotz
*
*/
public class JMapViewer extends JPanel implements TileLoaderListener {
private static final long serialVersionUID = 1L;
/**
* Vectors for clock-wise tile painting
*/
protected static final Point[] move = { new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1) };
public static final int MAX_ZOOM = 22;
public static final int MIN_ZOOM = 0;
protected List<MapMarker> mapMarkerList;
protected List<MapRectangle> mapRectangleList;
protected boolean mapMarkersVisible;
protected boolean mapRectanglesVisible;
protected boolean tileGridVisible;
protected TileController tileController;
/**
* x- and y-position of the center of this map-panel on the world map
* denoted in screen pixel regarding the current zoom level.
*/
protected Point center;
/**
* Current zoom level
*/
protected int zoom;
protected JSlider zoomSlider;
protected JButton zoomInButton;
protected JButton zoomOutButton;
/**
* Creates a standard {@link JMapViewer} instance that can be controlled via
* mouse: hold right mouse button for moving, double click left mouse button
* or use mouse wheel for zooming. Loaded tiles are stored the
* {@link MemoryTileCache} and the tile loader uses 4 parallel threads for
* retrieving the tiles.
*/
public JMapViewer() {
this(new MemoryTileCache(), 4);
new DefaultMapController(this);
}
public JMapViewer(TileCache tileCache, int downloadThreadCount) {
super();
tileController = new TileController(new OsmTileSource.Mapnik(), tileCache, this);
mapMarkerList = new LinkedList<MapMarker>();
mapRectangleList = new LinkedList<MapRectangle>();
mapMarkersVisible = true;
mapRectanglesVisible = true;
tileGridVisible = false;
setLayout(null);
initializeZoomSlider();
setMinimumSize(new Dimension(Tile.SIZE, Tile.SIZE));
setPreferredSize(new Dimension(400, 400));
setDisplayPositionByLatLon(50, 9, 3);
}
protected void initializeZoomSlider() {
zoomSlider = new JSlider(MIN_ZOOM, tileController.getTileSource().getMaxZoom());
zoomSlider.setOrientation(JSlider.VERTICAL);
zoomSlider.setBounds(10, 10, 30, 150);
zoomSlider.setOpaque(false);
zoomSlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
setZoom(zoomSlider.getValue());
}
});
add(zoomSlider);
int size = 18;
try {
ImageIcon icon = new ImageIcon(getClass().getResource("images/plus.png"));
zoomInButton = new JButton(icon);
} catch (Exception e) {
zoomInButton = new JButton("+");
zoomInButton.setFont(new Font("sansserif", Font.BOLD, 9));
zoomInButton.setMargin(new Insets(0, 0, 0, 0));
}
zoomInButton.setBounds(4, 155, size, size);
zoomInButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
zoomIn();
}
});
add(zoomInButton);
try {
ImageIcon icon = new ImageIcon(getClass().getResource("images/minus.png"));
zoomOutButton = new JButton(icon);
} catch (Exception e) {
zoomOutButton = new JButton("-");
zoomOutButton.setFont(new Font("sansserif", Font.BOLD, 9));
zoomOutButton.setMargin(new Insets(0, 0, 0, 0));
}
zoomOutButton.setBounds(8 + size, 155, size, size);
zoomOutButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
zoomOut();
}
});
add(zoomOutButton);
}
/**
* Changes the map pane so that it is centered on the specified coordinate
* at the given zoom level.
*
* @param lat
* latitude of the specified coordinate
* @param lon
* longitude of the specified coordinate
* @param zoom
* {@link #MIN_ZOOM} <= zoom level <= {@link #MAX_ZOOM}
*/
public void setDisplayPositionByLatLon(double lat, double lon, int zoom) {
setDisplayPositionByLatLon(new Point(getWidth() / 2, getHeight() / 2), lat, lon, zoom);
}
/**
* Changes the map pane so that the specified coordinate at the given zoom
* level is displayed on the map at the screen coordinate
* <code>mapPoint</code>.
*
* @param mapPoint
* point on the map denoted in pixels where the coordinate should
* be set
* @param lat
* latitude of the specified coordinate
* @param lon
* longitude of the specified coordinate
* @param zoom
* {@link #MIN_ZOOM} <= zoom level <=
* {@link TileSource#getMaxZoom()}
*/
public void setDisplayPositionByLatLon(Point mapPoint, double lat, double lon, int zoom) {
int x = OsmMercator.LonToX(lon, zoom);
int y = OsmMercator.LatToY(lat, zoom);
setDisplayPosition(mapPoint, x, y, zoom);
}
public void setDisplayPosition(int x, int y, int zoom) {
setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), x, y, zoom);
}
public void setDisplayPosition(Point mapPoint, int x, int y, int zoom) {
if (zoom > tileController.getTileSource().getMaxZoom() || zoom < MIN_ZOOM)
return;
// Get the plain tile number
Point p = new Point();
p.x = x - mapPoint.x + getWidth() / 2;
p.y = y - mapPoint.y + getHeight() / 2;
center = p;
setIgnoreRepaint(true);
try {
int oldZoom = this.zoom;
this.zoom = zoom;
if (oldZoom != zoom)
zoomChanged(oldZoom);
if (zoomSlider.getValue() != zoom)
zoomSlider.setValue(zoom);
} finally {
setIgnoreRepaint(false);
repaint();
}
}
/**
* Sets the displayed map pane and zoom level so that all map markers are
* visible.
*/
public void setDisplayToFitMapMarkers() {
if (mapMarkerList == null || mapMarkerList.size() == 0)
return;
int x_min = Integer.MAX_VALUE;
int y_min = Integer.MAX_VALUE;
int x_max = Integer.MIN_VALUE;
int y_max = Integer.MIN_VALUE;
int mapZoomMax = tileController.getTileSource().getMaxZoom();
for (MapMarker marker : mapMarkerList) {
int x = OsmMercator.LonToX(marker.getLon(), mapZoomMax);
int y = OsmMercator.LatToY(marker.getLat(), mapZoomMax);
x_max = Math.max(x_max, x);
y_max = Math.max(y_max, y);
x_min = Math.min(x_min, x);
y_min = Math.min(y_min, y);
}
int height = Math.max(0, getHeight());
int width = Math.max(0, getWidth());
// System.out.println(x_min + " < x < " + x_max);
// System.out.println(y_min + " < y < " + y_max);
// System.out.println("tiles: " + width + " " + height);
int newZoom = mapZoomMax;
int x = x_max - x_min;
int y = y_max - y_min;
while (x > width || y > height) {
// System.out.println("zoom: " + zoom + " -> " + x + " " + y);
newZoom--;
x >>= 1;
y >>= 1;
}
x = x_min + (x_max - x_min) / 2;
y = y_min + (y_max - y_min) / 2;
int z = 1 << (mapZoomMax - newZoom);
x /= z;
y /= z;
setDisplayPosition(x, y, newZoom);
}
/**
* Sets the displayed map pane and zoom level so that all map markers are
* visible.
*/
public void setDisplayToFitMapRectangle() {
if (mapRectangleList == null || mapRectangleList.size() == 0) {
return;
}
int x_min = Integer.MAX_VALUE;
int y_min = Integer.MAX_VALUE;
int x_max = Integer.MIN_VALUE;
int y_max = Integer.MIN_VALUE;
int mapZoomMax = tileController.getTileSource().getMaxZoom();
for (MapRectangle rectangle : mapRectangleList) {
x_max = Math.max(x_max, OsmMercator.LonToX(rectangle.getBottomRight().getLon(), mapZoomMax));
y_max = Math.max(y_max, OsmMercator.LatToY(rectangle.getTopLeft().getLat(), mapZoomMax));
x_min = Math.min(x_min, OsmMercator.LonToX(rectangle.getTopLeft().getLon(), mapZoomMax));
y_min = Math.min(y_min, OsmMercator.LatToY(rectangle.getBottomRight().getLat(), mapZoomMax));
}
int height = Math.max(0, getHeight());
int width = Math.max(0, getWidth());
// System.out.println(x_min + " < x < " + x_max);
// System.out.println(y_min + " < y < " + y_max);
// System.out.println("tiles: " + width + " " + height);
int newZoom = mapZoomMax;
int x = x_max - x_min;
int y = y_max - y_min;
while (x > width || y > height) {
// System.out.println("zoom: " + zoom + " -> " + x + " " + y);
newZoom--;
x >>= 1;
y >>= 1;
}
x = x_min + (x_max - x_min) / 2;
y = y_min + (y_max - y_min) / 2;
int z = 1 << (mapZoomMax - newZoom);
x /= z;
y /= z;
setDisplayPosition(x, y, newZoom);
}
public Coordinate getPosition() {
double lon = OsmMercator.XToLon(center.x, zoom);
double lat = OsmMercator.YToLat(center.y, zoom);
return new Coordinate(lat, lon);
}
public Coordinate getPosition(Point mapPoint) {
int x = center.x + mapPoint.x - getWidth() / 2;
int y = center.y + mapPoint.y - getHeight() / 2;
double lon = OsmMercator.XToLon(x, zoom);
double lat = OsmMercator.YToLat(y, zoom);
return new Coordinate(lat, lon);
}
public Coordinate getPosition(int mapPointX, int mapPointY) {
int x = center.x + mapPointX - getWidth() / 2;
int y = center.y + mapPointY - getHeight() / 2;
double lon = OsmMercator.XToLon(x, zoom);
double lat = OsmMercator.YToLat(y, zoom);
return new Coordinate(lat, lon);
}
/**
* Calculates the position on the map of a given coordinate
*
* @param lat
* @param lon
* @param checkOutside
* @return point on the map or <code>null</code> if the point is not visible and checkOutside set to <code>true</code>
*/
public Point getMapPosition(double lat, double lon, boolean checkOutside) {
int x = OsmMercator.LonToX(lon, zoom);
int y = OsmMercator.LatToY(lat, zoom);
x -= center.x - getWidth() / 2;
y -= center.y - getHeight() / 2;
if (checkOutside) {
if (x < 0 || y < 0 || x > getWidth() || y > getHeight()) {
return null;
}
}
return new Point(x, y);
}
/**
* Calculates the position on the map of a given coordinate
*
* @param lat
* @param lon
* @return point on the map or <code>null</code> if the point is not visible
*/
public Point getMapPosition(double lat, double lon) {
return getMapPosition(lat, lon, true);
}
/**
* Calculates the position on the map of a given coordinate
*
* @param coord
* @return point on the map or <code>null</code> if the point is not visible
*/
public Point getMapPosition(Coordinate coord) {
if (coord != null) {
return getMapPosition(coord.getLat(), coord.getLon());
} else {
return null;
}
}
/**
* Calculates the position on the map of a given coordinate
*
* @param coord
* @return point on the map or <code>null</code> if the point is not visible and checkOutside set to <code>true</code>
*/
public Point getMapPosition(Coordinate coord, boolean checkOutside) {
if (coord != null) {
return getMapPosition(coord.getLat(), coord.getLon(), checkOutside);
} else {
return null;
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int iMove = 0;
int tilex = center.x / Tile.SIZE;
int tiley = center.y / Tile.SIZE;
int off_x = (center.x % Tile.SIZE);
int off_y = (center.y % Tile.SIZE);
int w2 = getWidth() / 2;
int h2 = getHeight() / 2;
int posx = w2 - off_x;
int posy = h2 - off_y;
int diff_left = off_x;
int diff_right = Tile.SIZE - off_x;
int diff_top = off_y;
int diff_bottom = Tile.SIZE - off_y;
boolean start_left = diff_left < diff_right;
boolean start_top = diff_top < diff_bottom;
if (start_top) {
if (start_left)
iMove = 2;
else
iMove = 3;
} else {
if (start_left)
iMove = 1;
else
iMove = 0;
} // calculate the visibility borders
int x_min = -Tile.SIZE;
int y_min = -Tile.SIZE;
int x_max = getWidth();
int y_max = getHeight();
// paint the tiles in a spiral, starting from center of the map
boolean painted = true;
int x = 0;
while (painted) {
painted = false;
for (int i = 0; i < 4; i++) {
if (i % 2 == 0)
x++;
for (int j = 0; j < x; j++) {
if (x_min <= posx && posx <= x_max && y_min <= posy && posy <= y_max) {
// tile is visible
Tile tile = tileController.getTile(tilex, tiley, zoom);
if (tile != null) {
painted = true;
tile.paint(g, posx, posy);
if (tileGridVisible)
g.drawRect(posx, posy, Tile.SIZE, Tile.SIZE);
}
}
Point p = move[iMove];
posx += p.x * Tile.SIZE;
posy += p.y * Tile.SIZE;
tilex += p.x;
tiley += p.y;
}
iMove = (iMove + 1) % move.length;
}
}
// outer border of the map
int mapSize = Tile.SIZE << zoom;
g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize);
// g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20);
if (mapRectanglesVisible && mapRectangleList != null) {
for (MapRectangle rectangle : mapRectangleList) {
Coordinate topLeft = rectangle.getTopLeft();
Coordinate bottomRight = rectangle.getBottomRight();
if (topLeft != null && bottomRight != null) {
Point pTopLeft = getMapPosition(topLeft.getLat(), topLeft.getLon(), false);
Point pBottomRight = getMapPosition(bottomRight.getLat(), bottomRight.getLon(), false);
if (pTopLeft != null && pBottomRight != null) {
rectangle.paint(g, pTopLeft, pBottomRight);
}
}
}
}
if (mapMarkersVisible && mapMarkerList != null) {
for (MapMarker marker : mapMarkerList) {
Point p = getMapPosition(marker.getLat(), marker.getLon());
if (p != null) {
marker.paint(g, p);
}
}
}
}
/**
* Moves the visible map pane.
*
* @param x
* horizontal movement in pixel.
* @param y
* vertical movement in pixel
*/
public void moveMap(int x, int y) {
center.x += x;
center.y += y;
repaint();
}
/**
* @return the current zoom level
*/
public int getZoom() {
return zoom;
}
/**
* Increases the current zoom level by one
*/
public void zoomIn() {
setZoom(zoom + 1);
}
/**
* Increases the current zoom level by one
*/
public void zoomIn(Point mapPoint) {
setZoom(zoom + 1, mapPoint);
}
/**
* Decreases the current zoom level by one
*/
public void zoomOut() {
setZoom(zoom - 1);
}
/**
* Decreases the current zoom level by one
*/
public void zoomOut(Point mapPoint) {
setZoom(zoom - 1, mapPoint);
}
public void setZoom(int zoom, Point mapPoint) {
if (zoom > tileController.getTileSource().getMaxZoom() || zoom < tileController.getTileSource().getMinZoom()
|| zoom == this.zoom)
return;
Coordinate zoomPos = getPosition(mapPoint);
tileController.cancelOutstandingJobs(); // Clearing outstanding load
// requests
setDisplayPositionByLatLon(mapPoint, zoomPos.getLat(), zoomPos.getLon(), zoom);
}
public void setZoom(int zoom) {
setZoom(zoom, new Point(getWidth() / 2, getHeight() / 2));
}
/**
* Every time the zoom level changes this method is called. Override it in
* derived implementations for adapting zoom dependent values. The new zoom
* level can be obtained via {@link #getZoom()}.
*
* @param oldZoom
* the previous zoom level
*/
protected void zoomChanged(int oldZoom) {
zoomSlider.setToolTipText("Zoom level " + zoom);
zoomInButton.setToolTipText("Zoom to level " + (zoom + 1));
zoomOutButton.setToolTipText("Zoom to level " + (zoom - 1));
zoomOutButton.setEnabled(zoom > tileController.getTileSource().getMinZoom());
zoomInButton.setEnabled(zoom < tileController.getTileSource().getMaxZoom());
}
public boolean isTileGridVisible() {
return tileGridVisible;
}
public void setTileGridVisible(boolean tileGridVisible) {
this.tileGridVisible = tileGridVisible;
repaint();
}
public boolean getMapMarkersVisible() {
return mapMarkersVisible;
}
/**
* Enables or disables painting of the {@link MapMarker}
*
* @param mapMarkersVisible
* @see #addMapMarker(MapMarker)
* @see #getMapMarkerList()
*/
public void setMapMarkerVisible(boolean mapMarkersVisible) {
this.mapMarkersVisible = mapMarkersVisible;
repaint();
}
public void setMapMarkerList(List<MapMarker> mapMarkerList) {
this.mapMarkerList = mapMarkerList;
repaint();
}
public List<MapMarker> getMapMarkerList() {
return mapMarkerList;
}
public void setMapRectangleList(List<MapRectangle> mapRectangleList) {
this.mapRectangleList = mapRectangleList;
repaint();
}
public List<MapRectangle> getMapRectangleList() {
return mapRectangleList;
}
public void addMapMarker(MapMarker marker) {
mapMarkerList.add(marker);
repaint();
}
public void addMapRectangle(MapRectangle rectangle) {
mapRectangleList.add(rectangle);
repaint();
}
public void removeMapRectangle(MapRectangle rectangle) {
mapRectangleList.remove(rectangle);
repaint();
}
public void setZoomContolsVisible(boolean visible) {
zoomSlider.setVisible(visible);
zoomInButton.setVisible(visible);
zoomOutButton.setVisible(visible);
}
public boolean getZoomContolsVisible() {
return zoomSlider.isVisible();
}
public void setTileSource(TileSource tileSource) {
if (tileSource.getMaxZoom() > MAX_ZOOM)
throw new RuntimeException("Maximum zoom level too high");
if (tileSource.getMinZoom() < MIN_ZOOM)
throw new RuntimeException("Minumim zoom level too low");
tileController.setTileSource(tileSource);
zoomSlider.setMinimum(tileSource.getMinZoom());
zoomSlider.setMaximum(tileSource.getMaxZoom());
tileController.cancelOutstandingJobs();
if (zoom > tileSource.getMaxZoom())
setZoom(tileSource.getMaxZoom());
repaint();
}
public void tileLoadingFinished(Tile tile, boolean success) {
repaint();
}
public boolean isMapRectanglesVisible() {
return mapRectanglesVisible;
}
/**
* Enables or disables painting of the {@link MapRectangle}
*
* @param mapMarkersVisible
* @see #addMapRectangle(MapRectangle)
* @see #getMapRectangleList()
*/
public void setMapRectanglesVisible(boolean mapRectanglesVisible) {
this.mapRectanglesVisible = mapRectanglesVisible;
repaint();
}
/* (non-Javadoc)
* @see org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener#getTileCache()
*/
public TileCache getTileCache() {
return tileController.getTileCache();
}
public void setTileLoader(TileLoader loader) {
tileController.setTileLoader(loader);
}
}