// License: GPL. For details, see Readme.txt file. package amodeus.amodeus.view.jmapviewer; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Point; import java.util.ArrayList; import java.util.List; import javax.swing.JComponent; import amodeus.amodeus.view.jmapviewer.interfaces.ICoordinate; import amodeus.amodeus.view.jmapviewer.interfaces.TileCache; import amodeus.amodeus.view.jmapviewer.interfaces.TileLoaderListener; import amodeus.amodeus.view.jmapviewer.interfaces.TileSource; import amodeus.amodeus.view.jmapviewer.tilesources.MapnikTileSource; /** Provides a simple panel that displays pre-rendered map tiles loaded from the * OpenStreetMap project. * * @author Jan Peter Stotz * @author Jason Huntley */ public class JMapViewer extends JComponent implements TileLoaderListener { /** Vectors for clock-wise tile painting */ private static final Point[] MOVE = { new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1) }; /** Maximum zoom level */ public static final int MAX_ZOOM = 22; /** Minimum zoom level */ public static final int MIN_ZOOM = 0; /** whether debug mode is enabled or not */ public static boolean DEBUG; /** option to reverse zoom direction with mouse wheel */ public static boolean ZOOMREVERSEWHEEL; // --- protected boolean tileGridVisible = false; // <- for debug purposes protected boolean scrollWrapEnabled; private transient TileController tileController; protected final List<AmodeusHeatMap> matsimHeatmaps = new ArrayList<>(); /** 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; public int mapAlphaCover = 192; public int mapGrayCover = 0; private transient TileSource tileSource; private transient AttributionSupport attribution = new AttributionSupport(); /** 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 in a * {@link MemoryTileCache} and the tile loader uses 4 parallel threads for * retrieving the tiles. */ public JMapViewer() { this(new MemoryTileCache()); new DefaultMapController(this); } /** Creates a new {@link JMapViewer} instance. * * @param tileCache * The cache where to store tiles */ JMapViewer(TileCache tileCache) { tileSource = MapnikTileSource.INSTANCE; tileController = new TileController(tileSource, tileCache, this); setLayout(null); setMinimumSize(new Dimension(tileSource.getTileSize(), tileSource.getTileSize())); setPreferredSize(new Dimension(400, 400)); setDisplayPosition(new Coordinate(50, 9), 3); } /** Changes the map pane so that it is centered on the specified coordinate * at the given zoom level. * * @param to * specified coordinate * @param zoom * {@link #MIN_ZOOM} <= zoom level <= {@link #MAX_ZOOM} */ public void setDisplayPosition(ICoordinate to, int zoom) { setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), to, 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 to * specified coordinate * @param zoom * {@link #MIN_ZOOM} <= zoom level <= * {@link TileSource#getMaxZoom()} */ public void setDisplayPosition(Point mapPoint, ICoordinate to, int zoom) { Point p = tileSource.latLonToXY(to, zoom); setDisplayPosition(mapPoint, p.x, p.y, zoom); } /** Sets the display position. * * @param x * X coordinate * @param y * Y coordinate * @param zoom * zoom level, between {@link #MIN_ZOOM} and {@link #MAX_ZOOM} */ void setDisplayPosition(int x, int y, int zoom) { setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), x, y, zoom); } /** Sets the display position. * * @param mapPoint * map point * @param x * X coordinate * @param y * Y coordinate * @param zoom * zoom level, between {@link #MIN_ZOOM} and {@link #MAX_ZOOM} */ 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; this.zoom = zoom; repaint(); } /** Calculates the latitude/longitude coordinate of the center of the * currently displayed map area. * * @return latitude / longitude */ public ICoordinate getPosition() { return tileSource.xyToLatLon(center, zoom); } /** Converts the relative pixel coordinate (regarding the top left corner of * the displayed map) into a latitude / longitude coordinate * * @param mapPoint * relative pixel coordinate regarding the top left corner of the * displayed map * @return latitude / longitude */ public ICoordinate getPosition(Point mapPoint) { return getPosition(mapPoint.x, mapPoint.y); } /** Converts the relative pixel coordinate (regarding the top left corner of * the displayed map) into a latitude / longitude coordinate * * @param mapPointX * X coordinate * @param mapPointY * Y coordinate * @return latitude / longitude */ private ICoordinate getPosition(int mapPointX, int mapPointY) { int x = center.x + mapPointX - getWidth() / 2; int y = center.y + mapPointY - getHeight() / 2; return tileSource.xyToLatLon(x, y, zoom); } /** Calculates the position on the map of a given coordinate * * @param lat * latitude * @param lon * longitude * @param checkOutside * check if the point is outside the displayed area * @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) { Point p = tileSource.latLonToXY(lat, lon, zoom); p.translate(-(center.x - getWidth() / 2), -(center.y - getHeight() / 2)); if (checkOutside && (p.x < 0 || p.y < 0 || p.x > getWidth() || p.y > getHeight())) { return null; } return p; } /** Calculates the position on the map of a given coordinate * * @param lat * latitude * @param lon * longitude * @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); } /** Gets the meter per pixel. * * @return the meter per pixel */ public double getMeterPerPixel() { Point origin = new Point(5, 5); Point center = new Point(getWidth() / 2, getHeight() / 2); double pDistance = center.distance(origin); ICoordinate originCoord = getPosition(origin); ICoordinate centerCoord = getPosition(center); double mDistance = tileSource.getDistance(originCoord.getLat(), originCoord.getLon(), centerCoord.getLat(), centerCoord.getLon()); return mDistance / pDistance; } @Override protected void paintComponent(Graphics g) { // super.paintComponent(g); int iMove = 0; int tilesize = tileSource.getTileSize(); int tilex = center.x / tilesize; int tiley = center.y / tilesize; int offsx = center.x % tilesize; int offsy = center.y % tilesize; int w2 = getWidth() / 2; int h2 = getHeight() / 2; int posx = w2 - offsx; int posy = h2 - offsy; int diffLeft = offsx; int diffRight = tilesize - offsx; int diffTop = offsy; int diffBottom = tilesize - offsy; boolean startLeft = diffLeft < diffRight; boolean startTop = diffTop < diffBottom; if (startTop) { if (startLeft) { iMove = 2; } else { iMove = 3; } } else { if (startLeft) { iMove = 1; } else { iMove = 0; } } // calculate the visibility borders int xMin = -tilesize; int yMin = -tilesize; int xMax = getWidth(); int yMax = getHeight(); // calculate the length of the grid (number of squares per edge) int gridLength = 1 << zoom; // paint the tiles in a spiral, starting from center of the map boolean painted = true; int x = 0; g.setFont(new Font(Font.DIALOG, Font.PLAIN, 10)); // jan added this while (painted) { painted = false; for (int i = 0; i < 4; i++) { if (i % 2 == 0) { x++; } for (int j = 0; j < x; j++) { if (xMin <= posx && posx <= xMax && yMin <= posy && posy <= yMax) { // tile is visible Tile tile; if (isScrollWrapEnabled()) { // in case tilex is out of bounds, grab the tile to use for wrapping int tilexWrap = ((tilex % gridLength) + gridLength) % gridLength; tile = tileController.getTile(tilexWrap, tiley, zoom); } else { tile = tileController.getTile(tilex, tiley, zoom); } if (tile != null) { tile.paint(g, posx, posy); // , tilesize, tilesize { int rgb = mapGrayCover; g.setColor(new Color(rgb, rgb, rgb, mapAlphaCover)); g.fillRect(posx, posy, 256, 256); } for (AmodeusHeatMap matsimHeatmap : matsimHeatmaps) matsimHeatmap.render(g, tile, zoom, posx, posy); if (isTileGridVisible()) { g.setColor(new Color(0, 0, 0, 32)); g.drawLine(posx + 1, posy, posx + tilesize, posy); g.drawLine(posx, posy + 1, posx, posy + tilesize); g.setColor(new Color(0, 0, 0, 128)); g.drawString(String.format("x=%d y=%d", tile.getXtile(), tile.getYtile()), posx, posy + 10); } } painted = true; } Point p = MOVE[iMove]; posx += p.x * tilesize; posy += p.y * tilesize; tilex += p.x; tiley += p.y; } iMove = (iMove + 1) % MOVE.length; } } // outer border of the map int mapSize = tilesize << zoom; if (scrollWrapEnabled) { g.drawLine(0, h2 - center.y, getWidth(), h2 - center.y); g.drawLine(0, h2 - center.y + mapSize, getWidth(), h2 - center.y + mapSize); } else { g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize); } // g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20); // keep x-coordinates from growing without bound if scroll-wrap is enabled if (scrollWrapEnabled) { center.x = center.x % mapSize; } attribution.paintAttribution(g, getWidth(), getHeight(), getPosition(0, 0), getPosition(getWidth(), getHeight()), zoom, this); } /** Moves the visible map pane. * * @param x * horizontal movement in pixel. * @param y * vertical movement in pixel */ public void moveMap(int x, int y) { tileController.cancelOutstandingJobs(); // Clear outstanding load 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 * * @param mapPoint * point to choose as center for new zoom level */ 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 * * @param mapPoint * point to choose as center for new zoom level */ public void zoomOut(Point mapPoint) { setZoom(zoom - 1, mapPoint); } /** Set the zoom level and center point for display * * @param zoom * new zoom level * @param mapPoint * point to choose as center for new zoom level */ public void setZoom(int zoom, Point mapPoint) { if (zoom > tileController.getTileSource().getMaxZoom() || zoom < tileController.getTileSource().getMinZoom() || zoom == this.zoom) return; ICoordinate zoomPos = getPosition(mapPoint); tileController.cancelOutstandingJobs(); // Clearing outstanding load // requests setDisplayPosition(mapPoint, zoomPos, zoom); } /** Set the zoom level * * @param zoom * new zoom level */ public void setZoom(int zoom) { setZoom(zoom, new Point(getWidth() / 2, getHeight() / 2)); } /** Determines whether the tile grid is visible or not. * * @return {@code true} if the tile grid is visible, {@code false} otherwise */ public boolean isTileGridVisible() { return tileGridVisible; } /** Sets whether the tile grid is visible or not. * * @param tileGridVisible * {@code true} if the tile grid is visible, {@code false} otherwise */ public void setTileGridVisible(boolean tileGridVisible) { this.tileGridVisible = tileGridVisible; repaint(); } /** Sets the tile source. * * @param tileSource * tile source */ 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("Minimum zoom level too low"); ICoordinate position = getPosition(); this.tileSource = tileSource; tileController.setTileSource(tileSource); tileController.cancelOutstandingJobs(); if (zoom > tileSource.getMaxZoom()) { setZoom(tileSource.getMaxZoom()); } attribution.initialize(tileSource); setDisplayPosition(position, zoom); repaint(); } public TileSource getTileSource() { return tileSource; } @Override public void tileLoadingFinished(Tile tile, boolean success) { tile.setLoaded(success); repaint(); } /** Determines whether scroll wrap is enabled or not. * * @return {@code true} if scroll wrap is enabled, {@code false} otherwise */ public boolean isScrollWrapEnabled() { return scrollWrapEnabled; } /** Sets whether scroll wrap is enabled or not. * * @param scrollWrapEnabled * {@code true} if scroll wrap is enabled, {@code false} otherwise */ public void setScrollWrapEnabled(boolean scrollWrapEnabled) { this.scrollWrapEnabled = scrollWrapEnabled; repaint(); } /** Returns attribution. * * @return attribution */ public AttributionSupport getAttribution() { return attribution; } }