package com.opendoorlogistics.codefromweb.jxmapviewer2;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.DesignMode;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.swing.JDesktopPane;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputAdapter;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.GeoPosition;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.Tile;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileFactory;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileFactoryInfo;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileListener;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.empty.EmptyTileFactory;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.painter.AbstractPainter;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.painter.Painter;
/**
* A fork of the JxMapViewer class from the JxMapViewer2 project which extends
* JDesktopPane instead of JPanel. This code is subject to the original
* copyrights of the JxMapViewer2 project. Other modifications have also been
* made.
*
* The original JxMapViewer javadoc header follows:
*
* A tile oriented map component that can easily be used with tile sources on
* the web like Google and Yahoo maps, satellite data such as NASA imagery, and
* also with file based sources like pre-processed NASA images. A known map
* provider can be used with the SLMapServerInfo, which will connect to a 2km
* resolution version of NASA's Blue Marble Next Generation imagery. @see
* SLMapServerInfo for more information.
*
* Note, the JXMapViewer has three center point properties. The
* <B>addressLocation</B> property represents an abstract center of the map.
* This would usually be something like the first item in a search result. It is
* a {@link GeoPosition}. The <b>centerPosition</b> property represents the
* current center point of the map. If the user pans the map then the
* centerPosition point will change but the <B>addressLocation</B> will not.
* Calling <B>recenterToAddressLocation()</B> will move the map back to that
* center address. The <B>center</B> property represents the same point as the
* centerPosition property, but as a Point2D in pixel space instead of a
* GeoPosition in lat/long space. Note that the center property is a Point2D in
* the entire world bitmap, not in the portion of the map currently visible. You
* can use the <B>getViewportBounds()</B> method to find the portion of the map
* currently visible and adjust your calculations accordingly. Changing the
* <B>center</B> property will change the <B>centerPosition</B> property and
* vice versa. All three properties are bound.
*
* @author Joshua.Marinacci@sun.com
* @see org.jdesktop.swingx.mapviewer.bmng.SLMapServerInfo
*/
public class DesktopPaneMapViewer extends JDesktopPane implements DesignMode {
private static final long serialVersionUID = -3530746298586937321L;
/**
* The zoom level. Generally a value between 1 and 15 (TODO Is this true for
* all the mapping worlds? What does this mean if some mapping system
* doesn't support the zoom level?
*/
private int zoomLevel = 1;
/**
* The position, in <I>map coordinates</I> of the center point. This is
* defined as the distance from the top and left edges of the map in pixels.
* Dragging the map component will change the center position. Zooming
* in/out will cause the center to be recalculated so as to remain in the
* center of the new "map".
*/
private Point2D center = new Point2D.Double(0, 0);
/**
* Indicates whether or not to draw the borders between tiles. Defaults to
* false. TODO Generally not very nice looking, very much a product of
* testing Consider whether this should really be a property or not.
*/
private boolean drawTileBorders = false;
/**
* Factory used by this component to grab the tiles necessary for painting
* the map.
*/
private TileFactory factory;
/**
* The overlay to delegate to for painting the "foreground" of the map
* component. This would include painting waypoints, day/night, etc. Also
* receives mouse events.
*/
private Painter<? super DesktopPaneMapViewer> overlay;
private boolean designTime;
private Image loadingImage;
/**
* Create a new JXMapViewer. By default it will use the EmptyTileFactory
*/
public DesktopPaneMapViewer() {
factory = new EmptyTileFactory();
// make a dummy loading image
try {
URL url = this.getClass().getResource("mapviewer/resources/loading.png");
this.setLoadingImage(ImageIO.read(url));
} catch (Throwable ex) {
// System.out.println("could not load 'loading.png'");
BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = img.createGraphics();
g2.setColor(Color.black);
g2.fillRect(0, 0, 16, 16);
g2.dispose();
this.setLoadingImage(img);
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
doPaintComponent(g);
}
// the method that does the actual painting
private void doPaintComponent(Graphics g) {/*
* if (isOpaque() ||
* isDesignTime()) {
* g.setColor(getBackground());
* g.fillRect(0,0,getWidth(),
* getHeight()); }
*/
if (isDesignTime()) {
// do nothing
} else {
int z = getZoom();
Rectangle viewportBounds = getViewportBounds();
drawMapTiles(g, z, viewportBounds);
drawOverlays(z, g, viewportBounds);
}
super.paintBorder(g);
}
/**
* Indicate that the component is being used at design time, such as in a
* visual editor like NetBeans' Matisse
*
* @param b
* indicates if the component is being used at design time
*/
@Override
public void setDesignTime(boolean b) {
this.designTime = b;
}
/**
* Indicates whether the component is being used at design time, such as in
* a visual editor like NetBeans' Matisse
*
* @return boolean indicating if the component is being used at design time
*/
@Override
public boolean isDesignTime() {
return designTime;
}
/**
* Draw the map tiles. This method is for implementation use only.
*
* @param g
* Graphics
* @param zoom
* zoom level to draw at
* @param viewportBounds
* the bounds to draw within
*/
protected void drawMapTiles(final Graphics g, final int zoom, Rectangle viewportBounds) {
int size = getTileFactory().getTileSize(zoom);
Dimension mapSize = getTileFactory().getMapSize(zoom);
// calculate the "visible" viewport area in tiles
int numWide = viewportBounds.width / size + 2;
int numHigh = viewportBounds.height / size + 2;
// TilePoint topLeftTile = getTileFactory().getTileCoordinate(
// new Point2D.Double(viewportBounds.x, viewportBounds.y));
TileFactoryInfo info = getTileFactory().getInfo();
int tpx = (int) Math.floor(viewportBounds.getX() / info.getTileSize(0));
int tpy = (int) Math.floor(viewportBounds.getY() / info.getTileSize(0));
// TilePoint topLeftTile = new TilePoint(tpx, tpy);
// p("top tile = " + topLeftTile);
// fetch the tiles from the factory and store them in the tiles cache
// attach the tileLoadListener
for (int x = 0; x <= numWide; x++) {
for (int y = 0; y <= numHigh; y++) {
int itpx = x + tpx;// topLeftTile.getX();
int itpy = y + tpy;// topLeftTile.getY();
// TilePoint point = new TilePoint(x + topLeftTile.getX(), y +
// topLeftTile.getY());
// only proceed if the specified tile point lies within the area
// being painted
if (g.getClipBounds().intersects(new Rectangle(itpx * size - viewportBounds.x, itpy * size - viewportBounds.y, size, size))) {
Tile tile = getTileFactory().getTile(itpx, itpy, zoom);
int ox = ((itpx * getTileFactory().getTileSize(zoom)) - viewportBounds.x);
int oy = ((itpy * getTileFactory().getTileSize(zoom)) - viewportBounds.y);
// if the tile is off the map to the north/south, then just
// don't paint anything
if (isTileOnMap(itpx, itpy, mapSize)) {
if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(ox, oy, size, size);
}
} else if (tile.isLoaded()) {
g.drawImage(tile.getImage(), ox, oy, null);
} else {
// Use tile at higher zoom level with 200% magnification
Tile superTile = getTileFactory().getTile(itpx / 2, itpy / 2, zoom + 1);
if (superTile.isLoaded()) {
int offX = (itpx % 2) * size / 2;
int offY = (itpy % 2) * size / 2;
g.drawImage(superTile.getImage(), ox, oy, ox + size, oy + size, offX, offY, offX + size / 2, offY + size / 2, null);
} else {
int imageX = (getTileFactory().getTileSize(zoom) - getLoadingImage().getWidth(null)) / 2;
int imageY = (getTileFactory().getTileSize(zoom) - getLoadingImage().getHeight(null)) / 2;
g.setColor(Color.GRAY);
g.fillRect(ox, oy, size, size);
g.drawImage(getLoadingImage(), ox + imageX, oy + imageY, null);
}
}
if (isDrawTileBorders()) {
g.setColor(Color.black);
g.drawRect(ox, oy, size, size);
g.drawRect(ox + size / 2 - 5, oy + size / 2 - 5, 10, 10);
g.setColor(Color.white);
g.drawRect(ox + 1, oy + 1, size, size);
String text = itpx + ", " + itpy + ", " + getZoom();
g.setColor(Color.BLACK);
g.drawString(text, ox + 10, oy + 30);
g.drawString(text, ox + 10 + 2, oy + 30 + 2);
g.setColor(Color.WHITE);
g.drawString(text, ox + 10 + 1, oy + 30 + 1);
}
}
}
}
}
private void drawOverlays(final int zoom, final Graphics g, final Rectangle viewportBounds) {
if (overlay != null) {
overlay.paint((Graphics2D) g, this, getWidth(), getHeight());
}
}
private boolean isTileOnMap(int x, int y, Dimension mapSize) {
return y >= mapSize.getHeight();
}
/**
* Sets the map overlay. This is a Painter<JXMapViewer> which will paint on
* top of the map. It can be used to draw waypoints, lines, or static
* overlays like text messages.
*
* @param overlay
* the map overlay to use
*/
public void setOverlayPainter(Painter<? super DesktopPaneMapViewer> overlay) {
Painter<? super DesktopPaneMapViewer> old = getOverlayPainter();
this.overlay = overlay;
PropertyChangeListener listener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getNewValue().equals(Boolean.TRUE)) {
repaint();
}
}
};
if (old instanceof AbstractPainter) {
AbstractPainter<?> ap = (AbstractPainter<?>) overlay;
ap.removePropertyChangeListener("dirty", listener);
}
if (overlay instanceof AbstractPainter) {
AbstractPainter<?> ap = (AbstractPainter<?>) overlay;
ap.addPropertyChangeListener("dirty", listener);
}
firePropertyChange("mapOverlay", old, getOverlayPainter());
repaint();
}
/**
* Gets the current map overlay
*
* @return the current map overlay
*/
public Painter<? super DesktopPaneMapViewer> getOverlayPainter() {
return overlay;
}
/**
* Returns the bounds of the viewport in pixels. This can be used to
* transform points into the world bitmap coordinate space.
*
* @return the bounds in <em>pixels</em> of the "view" of this map
*/
public Rectangle getViewportBounds() {
return calculateViewportBounds(getCenter());
}
private Rectangle calculateViewportBounds(Point2D centr) {
Insets insets = getInsets();
// calculate the "visible" viewport area in pixels
int viewportWidth = getWidth() - insets.left - insets.right;
int viewportHeight = getHeight() - insets.top - insets.bottom;
double viewportX = (centr.getX() - viewportWidth / 2);
double viewportY = (centr.getY() - viewportHeight / 2);
return new Rectangle((int) viewportX, (int) viewportY, viewportWidth, viewportHeight);
}
/**
* Set the current zoom level
*
* @param zoom
* the new zoom level
*/
public void setZoom(int zoom) {
if (zoom == this.zoomLevel) {
return;
}
TileFactoryInfo info = getTileFactory().getInfo();
// don't repaint if we are out of the valid zoom levels
if (info != null && (zoom < info.getMinimumZoomLevel() || zoom > info.getMaximumZoomLevel())) {
return;
}
// if(zoom >= 0 && zoom <= 15 && zoom != this.zoom) {
int oldzoom = this.zoomLevel;
Point2D oldCenter = getCenter();
Dimension oldMapSize = getTileFactory().getMapSize(oldzoom);
this.zoomLevel = zoom;
this.firePropertyChange("zoom", oldzoom, zoom);
Dimension mapSize = getTileFactory().getMapSize(zoom);
setCenter(new Point2D.Double(oldCenter.getX() * (mapSize.getWidth() / oldMapSize.getWidth()), oldCenter.getY()
* (mapSize.getHeight() / oldMapSize.getHeight())));
repaint();
}
/**
* Gets the current zoom level
*
* @return the current zoom level
*/
public int getZoom() {
return this.zoomLevel;
}
/**
* Gets the current address location of the map. This property does not
* change when the user pans the map. This property is bound.
*
* @return the current map location (address)
*/
/**
* Indicates if the tile borders should be drawn. Mainly used for debugging.
*
* @return the value of this property
*/
public boolean isDrawTileBorders() {
return drawTileBorders;
}
/**
* Set if the tile borders should be drawn. Mainly used for debugging.
*
* @param drawTileBorders
* new value of this drawTileBorders
*/
public void setDrawTileBorders(boolean drawTileBorders) {
boolean old = isDrawTileBorders();
this.drawTileBorders = drawTileBorders;
firePropertyChange("drawTileBorders", old, isDrawTileBorders());
repaint();
}
/**
* A property indicating the center position of the map
*
* @param geoPosition
* the new property value
*/
public void setCenterPosition(GeoPosition geoPosition) {
GeoPosition oldVal = getCenterPosition();
setCenter(getTileFactory().geoToPixel(geoPosition, zoomLevel));
repaint();
GeoPosition newVal = getCenterPosition();
firePropertyChange("centerPosition", oldVal, newVal);
}
/**
* A property indicating the center position of the map
*
* @return the current center position
*/
public GeoPosition getCenterPosition() {
return getTileFactory().pixelToGeo(getCenter(), zoomLevel);
}
/**
* Get the current factory
*
* @return the current property value
*/
public TileFactory getTileFactory() {
return factory;
}
/**
* Set the current tile factory (must not be <code>null</code>)
*
* @param factory
* the new property value
*/
public void setTileFactory(TileFactory factory) {
if (factory == null)
throw new NullPointerException("factory must not be null");
this.factory.removeTileListener(tileLoadListener);
this.factory.dispose();
this.factory = factory;
this.setZoom(factory.getInfo().getDefaultZoomLevel());
factory.addTileListener(tileLoadListener);
repaint();
}
/**
* A property for an image which will be display when an image is still
* loading.
*
* @return the current property value
*/
public Image getLoadingImage() {
return loadingImage;
}
/**
* A property for an image which will be display when an image is still
* loading.
*
* @param loadingImage
* the new property value
*/
public void setLoadingImage(Image loadingImage) {
this.loadingImage = loadingImage;
}
/**
* Gets the current pixel center of the map. This point is in the global
* bitmap coordinate system, not as lat/longs.
*
* @return the current center of the map as a pixel value
*/
public Point2D getCenter() {
return center;
}
/**
* Sets the new center of the map in pixel coordinates.
*
* @param center
* the new center of the map in pixel coordinates
*/
public void setCenter(Point2D center) {
Point2D old = this.getCenter();
double centerX = center.getX();
double centerY = center.getY();
Dimension mapSize = getTileFactory().getMapSize(getZoom());
int mapHeight = (int) mapSize.getHeight() * getTileFactory().getTileSize(getZoom());
int mapWidth = (int) mapSize.getWidth() * getTileFactory().getTileSize(getZoom());
Insets insets = getInsets();
int viewportHeight = getHeight() - insets.top - insets.bottom;
// don't let the user pan over the top edge
Rectangle newVP = calculateViewportBounds(center);
if (newVP.getY() < 0) {
centerY = viewportHeight / 2;
}
// don't let the user pan over the bottom edge
if (newVP.getY() + newVP.getHeight() > mapHeight) {
centerY = mapHeight - viewportHeight / 2;
}
// if map is to small then just center it vert
if (mapHeight < newVP.getHeight()) {
centerY = mapHeight / 2;// viewportHeight/2;// - mapHeight/2;
}
// If center is outside (0, 0,mapWidth, mapHeight)
// compute modulo to get it back in.
{
centerX = centerX % mapWidth;
centerY = centerY % mapHeight;
if (centerX < 0)
centerX += mapWidth;
if (centerY < 0)
centerY += mapHeight;
}
GeoPosition oldGP = this.getCenterPosition();
this.center = new Point2D.Double(centerX, centerY);
firePropertyChange("center", old, this.center);
firePropertyChange("centerPosition", oldGP, this.getCenterPosition());
repaint();
}
/**
* Calculates a zoom level so that all points in the specified set will be
* visible on screen. This is useful if you have a bunch of points in an
* area like a city and you want to zoom out so that the entire city and
* it's points are visible without panning.
*
* @param positions
* A set of GeoPositions to calculate the new zoom from
*/
public void calculateZoomFrom(Set<GeoPosition> positions) {
// u.p("calculating a zoom based on: ");
// u.p(positions);
if (positions.size() < 2) {
return;
}
int zoom = getZoom();
Rectangle2D rect = generateBoundingRect(positions, zoom);
// Rectangle2D viewport = map.getViewportBounds();
int count = 0;
while (!getViewportBounds().contains(rect)) {
// u.p("not contained");
Point2D centr = new Point2D.Double(rect.getX() + rect.getWidth() / 2, rect.getY() + rect.getHeight() / 2);
GeoPosition px = getTileFactory().pixelToGeo(centr, zoom);
// u.p("new geo = " + px);
setCenterPosition(px);
count++;
if (count > 30)
break;
if (getViewportBounds().contains(rect)) {
// u.p("did it finally");
break;
}
zoom = zoom + 1;
if (zoom > 15) {
break;
}
setZoom(zoom);
rect = generateBoundingRect(positions, zoom);
}
}
private Rectangle2D generateBoundingRect(final Set<GeoPosition> positions, int zoom) {
Point2D point1 = getTileFactory().geoToPixel(positions.iterator().next(), zoom);
Rectangle2D rect = new Rectangle2D.Double(point1.getX(), point1.getY(), 0, 0);
for (GeoPosition pos : positions) {
Point2D point = getTileFactory().geoToPixel(pos, zoom);
rect.add(point);
}
return rect;
}
// a property change listener which forces repaints when tiles finish
// loading
private TileListener tileLoadListener = new TileListener() {
@Override
public void tileLoaded(Tile tile) {
if (tile.getZoom() == getZoom()) {
repaint();
/*
* this optimization doesn't save much and it doesn't work if
* you wrap around the world Rectangle viewportBounds =
* getViewportBounds(); TilePoint tilePoint = t.getLocation();
* Point point = new Point(tilePoint.getX() *
* getTileFactory().getTileSize(), tilePoint.getY() *
* getTileFactory().getTileSize()); Rectangle tileRect = new
* Rectangle(point, new
* Dimension(getTileFactory().getTileSize(),
* getTileFactory().getTileSize())); if
* (viewportBounds.intersects(tileRect)) { //convert tileRect
* from world space to viewport space repaint(new Rectangle(
* tileRect.x - viewportBounds.x, tileRect.y - viewportBounds.y,
* tileRect.width, tileRect.height )); }
*/
}
}
};
/**
* Converts the specified GeoPosition to a point in the JXMapViewer's local
* coordinate space. This method is especially useful when drawing lat/long
* positions on the map.
*
* @param pos
* a GeoPosition on the map
* @return the point in the local coordinate space of the map
*/
public Point2D convertGeoPositionToPoint(GeoPosition pos) {
// convert from geo to world bitmap
Point2D pt = getTileFactory().geoToPixel(pos, getZoom());
// convert from world bitmap to local
Rectangle bounds = getViewportBounds();
return new Point2D.Double(pt.getX() - bounds.getX(), pt.getY() - bounds.getY());
}
/**
* Converts the specified Point2D in the JXMapViewer's local coordinate
* space to a GeoPosition on the map. This method is especially useful for
* determining the GeoPosition under the mouse cursor.
*
* @param pt
* a point in the local coordinate space of the map
* @return the point converted to a GeoPosition
*/
public GeoPosition convertPointToGeoPosition(Point2D pt) {
// convert from local to world bitmap
Rectangle bounds = getViewportBounds();
Point2D pt2 = new Point2D.Double(pt.getX() + bounds.getX(), pt.getY() + bounds.getY());
// convert from world bitmap to geo
GeoPosition pos = getTileFactory().pixelToGeo(pt2, getZoom());
return pos;
}
/**
* Used to pan using press and drag mouse gestures
*
* @author joshy
*/
public class DesktopPanePanMouseInputListener extends MouseInputAdapter {
private Point prev;
@Override
public void mousePressed(MouseEvent evt) {
prev = evt.getPoint();
}
@Override
public void mouseDragged(MouseEvent evt) {
if (!SwingUtilities.isLeftMouseButton(evt))
return;
Point current = evt.getPoint();
double x = getCenter().getX() - (current.x - prev.x);
double y = getCenter().getY() - (current.y - prev.y);
int maxHeight = (int) (getTileFactory().getMapSize(getZoom()).getHeight() * getTileFactory().getTileSize(getZoom()));
if (y > maxHeight) {
y = maxHeight;
}
prev = current;
setCenter(new Point2D.Double(x, y));
repaint();
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
}
@Override
public void mouseReleased(MouseEvent evt) {
if (!SwingUtilities.isLeftMouseButton(evt))
return;
prev = null;
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
@Override
public void mouseEntered(MouseEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
requestFocusInWindow();
}
});
}
}
/**
* zooms to the current mouse cursor using the mouse wheel
*
* @author Martin Steiger
*/
public class DesktopPaneZoomMouseWheelListenerCursor implements MouseWheelListener {
@Override
public void mouseWheelMoved(MouseWheelEvent evt) {
Point current = evt.getPoint();
Rectangle bound = getViewportBounds();
double dx = current.x - bound.width / 2;
double dy = current.y - bound.height / 2;
Dimension oldMapSize = getTileFactory().getMapSize(getZoom());
setZoom(getZoom() + evt.getWheelRotation());
Dimension mapSize = getTileFactory().getMapSize(getZoom());
Point2D center = getCenter();
double dzw = (mapSize.getWidth() / oldMapSize.getWidth());
double dzh = (mapSize.getHeight() / oldMapSize.getHeight());
double x = center.getX() + dx * (dzw - 1);
double y = center.getY() + dy * (dzh - 1);
setCenter(new Point2D.Double(x, y));
}
}
}