package com.aerodynelabs.map;
//XXX zoomIn/Out to mouse location
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.util.Collection;
import java.util.Hashtable;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
/**
* A tile based slippy map.
*
* @author Ethan Harstad
*
*/
@SuppressWarnings("serial")
public class MapPanel extends JPanel implements MouseListener, MouseMotionListener{
private static final String attribution1 =
"Tiles courtesy of MapQuest.com\n";
private static final String attribution2 =
"Map data \u00a9 OpenStreetMap contributors, CC-BY-SA";
private double lat, lon;
private int zoom;
private Point mouseDown;
protected Hashtable<String, MapOverlay> overlays;
private TileServer server;
/**
* A map centered on Ames, IA using Mapquest tiles.
*/
public MapPanel() {
this(42.01, -93.57, 11, "http://otile1.mqcdn.com/tiles/1.0.0/osm/", 18);
}
/**
* A map centered on the given coordinates with the given zoom using Mapquest tiles.
* @param lat
* @param lon
* @param zoom
*/
public MapPanel(double lat, double lon, int zoom) {
this(lat, lon, zoom, "http://otile1.mqcdn.com/tiles/1.0.0/osm/", 18);
}
/**
* A map centered on the given coordinates with the given zoom using the given tileset.
* @param lat
* @param lon
* @param zoom
* @param url
*/
public MapPanel(double lat, double lon, int zoom, String url) {
this(lat, lon, zoom, url, 17);
}
/**
* A map centered on the given coordinates with the given zoom and max zoom using the given tileset.
* @param lat
* @param lon
* @param zoom
* @param url
* @param maxZoom
*/
public MapPanel(double lat, double lon, int zoom, String url, int maxZoom) {
super.setPreferredSize(new Dimension(640, 480));
server = new TileServer(url, maxZoom, this);
overlays = new Hashtable<String, MapOverlay>();
setZoom(zoom);
setCenter(lat, lon);
addMouseListener(this);
addMouseMotionListener(this);
setBorder(BorderFactory.createLoweredBevelBorder());
}
/**
* Add an overlay to the map.
* @param name
* @param overlay
*/
public void addOverlay(String name, MapOverlay overlay) {
overlays.put(name, overlay);
}
/**
* Add an overlay to the map.
* @param overlay
*/
public void addOverlay(MapOverlay overlay) {
overlays.put(overlay.getName(), overlay);
}
/**
* Set the zoom level of the map.
* @param zoom
*/
protected void setZoom(int zoom) {
this.zoom = zoom;
repaint();
}
/**
* Get the zoom level of the map.
* @return
*/
public int getZoom() {
return zoom;
}
/**
* Zoom in
*/
protected void zoomIn() {
setZoom(zoom + 1);
}
/**
* Zoom out
*/
protected void zoomOut() {
setZoom(zoom - 1);
}
/**
* Set the center coordinates of the map.
* @param lat
* @param lon
*/
public void setCenter(double lat, double lon) {
this.lat = lat;
this.lon = lon;
repaint();
}
/**
* Get the western point of the given tile.
* @param x
* @param zoom
* @return
*/
protected static double tile2lon(int x, int zoom) {
return x / Math.pow(2.0, zoom) * 360.0 - 180;
}
/**
* Get the northern point of the given tile.
* @param y
* @param zoom
* @return
*/
protected static double tile2lat(int y, int zoom) {
double n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, zoom);
return Math.toDegrees(Math.atan(Math.sinh(n)));
}
/**
* Get the tile associated with the given latitude.
* @param lat
* @param zoom
* @return
*/
protected static int lat2tile(double lat, int zoom) {
return (int)Math.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2 * (1<<zoom));
}
/**
* Get the tile associated with the given longitude.
* @param lon
* @param zoom
* @return
*/
protected static int lon2tile(double lon, int zoom) {
return (int)Math.floor((lon + 180) / 360 * (1<<zoom));
}
/**
* Convert the given latitude to map screen space.
* @param lat
* @return
*/
public int getLatPos(double lat) {
return lat2pos(lat, zoom) - lat2pos(this.lat, zoom);
}
/**
* Convert the given longitude to map screen space.
* @param lon
* @return
*/
public int getLonPos(double lon) {
return lon2pos(lon, zoom) - lon2pos(this.lon, zoom);
}
/**
* Convert the given longitude to map pixel space.
* @param lon
* @param zoom
* @return
*/
public static int lon2pos(double lon, int zoom) {
double max = 256 * (1 << zoom);
return (int)Math.floor((lon + 180) / 360 * max);
}
/**
* Convert the given latitude to map pixel space.
* @param lat
* @param zoom
* @return
*/
public static int lat2pos(double lat, int zoom) {
double max = 256 * (1 << zoom);
double rlat = Math.toRadians(lat);
return (int)Math.floor((1 - Math.log(Math.tan(rlat) + 1 / Math.cos(rlat)) / Math.PI) / 2 * max);
}
/**
* Get the northern bound of the map window.
* @return
*/
public double getNorthBound() {
int sy = lat2tile(lat, zoom);
double sLat = tile2lat(sy, zoom);
double dLat = (tile2lat(sy + 1, zoom) - sLat) / 256.0;
return lat - (dLat * (this.getHeight() / 2.0));
}
/**
* Get the southern bound of the map window.
* @return
*/
public double getSouthBound() {
int sy = lat2tile(lat, zoom);
double sLat = tile2lat(sy, zoom);
double dLat = (tile2lat(sy + 1, zoom) - sLat) / 256.0;
return lat + (dLat * (this.getHeight() / 2.0));
}
/**
* Get the eastern bound of the map window.
* @return
*/
public double getEastBound() {
int sx = lon2tile(lon, zoom);
double sLon = tile2lon(sx, zoom);
double dLon = (tile2lon(sx + 1, zoom) - sLon) / 256.0;
return lon + (dLon * (this.getWidth() / 2.0));
}
/**
* Get the western bound of the map window.
* @return
*/
public double getWestBound() {
int sx = lon2tile(lon, zoom);
double sLon = tile2lon(sx, zoom);
double dLon = (tile2lon(sx + 1, zoom) - sLon) / 256.0;
return lon - (dLon * (this.getWidth() / 2.0));
}
@Override
protected void paintComponent(Graphics g0) {
super.paintComponents(g0);
Graphics2D g = (Graphics2D)g0.create();
int width = this.getWidth();
int height = this.getHeight();
g.translate(width/2, height/2);
int sx = lon2tile(lon, zoom);
int sy = lat2tile(lat, zoom);
int nx = ((width / 256) + 2) / 2;
int ny = ((height / 256) + 2) / 2;
double slon = tile2lon(sx, zoom);
double slat = tile2lat(sy, zoom);
int ox = (int)((256/(tile2lon(sx+1, zoom)-slon))*(lon-slon)+0.5);
int oy = (int)((256/(tile2lat(sy+1, zoom)-slat))*(lat-slat)+0.5);
for(int i = -nx; i <= nx; i++) {
for(int j = -ny; j <= ny; j++) {
int dx = i * 256 - ox;
int dy = j * 256 - oy;
BufferedImage tile = server.getTile(sx+i, sy+j, zoom);
g.drawImage(tile, dx, dy, null);
}
}
Collection<MapOverlay> c = overlays.values();
for(MapOverlay overlay : c) overlay.drawOverlay(this, g);
Font font = new Font("SansSerif", Font.PLAIN, 10);
FontMetrics metrics = super.getFontMetrics(font);
g.setFont(font);
int x = metrics.stringWidth(attribution1);
int y = metrics.getHeight();
g.drawString(attribution1, width/2 - x - 5, height/2 - y - 5);
x = metrics.stringWidth(attribution2);
g.drawString(attribution2, width/2 - x - 5, height/2 - 5);
}
/**
* The map has been removed from the screen. Close down.
*/
@Override
public void removeNotify() {
super.removeNotify();
// FIXME Disabled due to spurious use
// server.close();
}
/**
* Move the map by the given number of pixels.
* @param tx
* @param ty
*/
protected void translateMap(int tx, int ty) {
int x0 = lon2tile(lon, zoom);
int y0 = lat2tile(lat, zoom);
double dLon = tile2lon(x0 + 1, zoom) - tile2lon(x0, zoom);
double dLat = tile2lat(y0 + 1, zoom) - tile2lat(y0, zoom);
double dx = -dLon / 256;
double dy = -dLat / 256;
setCenter(ty * dy + lat, tx * dx + lon);
}
/**
* Handle the mouse drag event.
*/
@Override
public void mouseDragged(MouseEvent e) {
int tx = e.getX() - mouseDown.x;
int ty = e.getY() - mouseDown.y;
mouseDown = e.getPoint();
translateMap(tx, ty);
}
@Override
public void mouseMoved(MouseEvent e) {
// Not needed
}
/**
* Handle the mouse click event.
*/
@Override
public void mouseClicked(MouseEvent e) {
if(e.getClickCount() == 2) {
if(e.getButton() == MouseEvent.BUTTON1) {
zoomIn();
e.consume();
} else if(e.getButton() == MouseEvent.BUTTON3) {
zoomOut();
e.consume();
}
}
}
@Override
public void mouseEntered(MouseEvent e) {
// Not needed
}
@Override
public void mouseExited(MouseEvent e) {
// Not needed
}
/**
* Handle the mouse pressed event.
*/
@Override
public void mousePressed(MouseEvent e) {
mouseDown = e.getPoint();
}
@Override
public void mouseReleased(MouseEvent e) {
// Not needed
}
/**
* Notify the map of a change, redraw the map.
*/
public void updateNotify() {
repaint();
}
public MapOverlay getOverylay(String name) {
return overlays.get(name);
}
}