/******************************************************************************* * Copyright (c) MOBAC developers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package mobac.gui.mapview; //License: GPL. Copyright 2008 by Jan Peter Stotz import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.Point2D; import java.util.ConcurrentModificationException; 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.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import mobac.gui.mapview.interfaces.MapLayer; import mobac.gui.mapview.interfaces.MapTileLayer; import mobac.gui.mapview.interfaces.TileLoaderListener; import mobac.gui.mapview.layer.DefaultMapTileLayer; import mobac.gui.mapview.layer.MapGridLayer; import mobac.program.interfaces.MapSource; import mobac.program.interfaces.MapSpace; import mobac.utilities.Utilities; import org.apache.log4j.Logger; /** * * 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; private static Logger log = Logger.getLogger(JMapViewer.class); /** * 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 TileLoader tileLoader; protected MemoryTileCache tileCache; protected MapSource mapSource; protected boolean usePlaceHolderTiles = true; protected boolean mapMarkersVisible; protected MapGridLayer mapGridLayer = null; protected List<MapTileLayer> mapTileLayers; public List<MapLayer> mapLayers; /** * 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 = new Point(); /** * Current zoom level */ protected int zoom; protected JSlider zoomSlider = new JSlider(0, 0); protected JButton zoomInButton; protected JButton zoomOutButton; protected JobDispatcher jobDispatcher; public JMapViewer(MapSource defaultMapSource, int downloadThreadCount) { super(); mapTileLayers = new LinkedList<MapTileLayer>(); mapLayers = new LinkedList<MapLayer>(); tileLoader = new TileLoader(this); tileCache = new MemoryTileCache(); jobDispatcher = JobDispatcher.getInstance(); mapMarkersVisible = true; setLayout(null); setMapSource(defaultMapSource); initializeZoomSlider(); setMinimumSize(new Dimension(256, 256)); setPreferredSize(new Dimension(400, 400)); setDisplayPositionByLatLon(50.0, 9.0, 1); } protected void initializeZoomSlider() { 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 = Utilities.loadResourceImageIcon("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 = Utilities.loadResourceImageIcon("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 MapSource#getMaxZoom()} */ public void setDisplayPositionByLatLon(Point mapPoint, double lat, double lon, int zoom) { zoom = Math.max(Math.min(zoom, mapSource.getMaxZoom()), mapSource.getMinZoom()); MapSpace mapSpace = mapSource.getMapSpace(); //int x = mapSpace.cLonToX(lon, zoom); //int y = mapSpace.cLatToY(lat, zoom); Point p = mapSpace.cLonLatToXY(lon, lat, zoom); int x = p.x; int y = p.y; 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 > mapSource.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 the two points (x1/y1) and (x2/y2) visible. Please note that * the coordinates have to be specified regarding {@link #MAX_ZOOM}. * * @param x1 * @param y1 * @param x2 * @param y2 */ public void setDisplayToFitPixelCoordinates(int x1, int y1, int x2, int y2) { int mapZoomMax = mapSource.getMaxZoom(); int height = Math.max(0, getHeight()); int width = Math.max(0, getWidth()); int newZoom = MAX_ZOOM; int x = Math.abs(x1 - x2); int y = Math.abs(y1 - y2); while (x > width || y > height || newZoom > mapZoomMax) { newZoom--; x >>= 1; y >>= 1; } // Do not select a zoom level that is unsupported by the current map // source newZoom = Math.max(mapSource.getMinZoom(), Math.min(mapSource.getMaxZoom(), newZoom)); x = Math.min(x2, x1) + Math.abs(x1 - x2) / 2; y = Math.min(y2, y1) + Math.abs(y1 - y2) / 2; int z = 1 << (MAX_ZOOM - newZoom); x /= z; y /= z; setDisplayPosition(x, y, newZoom); } public Point2D.Double getPosition() { MapSpace mapSpace = mapSource.getMapSpace(); //double lon = mapSpace.cXToLon(center.x, zoom); //double lat = mapSpace.cYToLat(center.y, zoom); //return new Point2D.Double(lat, lon); Point2D.Double p = mapSpace.cXYToLonLat(center.x, center.y, zoom); return new Point2D.Double(p.y, p.x); } public Point2D.Double getPosition(Point mapPoint) { MapSpace mapSpace = mapSource.getMapSpace(); int x = center.x + mapPoint.x - getWidth() / 2; int y = center.y + mapPoint.y - getHeight() / 2; //double lon = mapSpace.cXToLon(x, zoom); //double lat = mapSpace.cYToLat(y, zoom); //return new Point2D.Double(lat, lon); Point2D.Double p = mapSpace.cXYToLonLat(x, y, zoom); return new Point2D.Double(p.y, p.x); } /** * 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) { MapSpace mapSpace = mapSource.getMapSpace(); //int x = mapSpace.cLonToX(lon, zoom); //int y = mapSpace.cLatToY(lat, zoom); Point p = mapSpace.cLonLatToXY(lon, lat, zoom); int x = p.x; int y = p.y; x -= center.x - getWidth() / 2; y -= center.y - getHeight() / 2; if (x < 0 || y < 0 || x > getWidth() || y > getHeight()) return null; return new Point(x, y); } @Override protected void paintComponent(Graphics graphics) { Graphics2D g = (Graphics2D) graphics; // if (mapIsMoving) { // mapIsMoving = false; // Doesn't look very pretty but is much more faster // g.copyArea(0, 0, getWidth(), getHeight(), -mapMoveX, -mapMoveY); // return; // } super.paintComponent(g); int iMove = 0; int tileSize = mapSource.getMapSpace().getTileSize(); int tilex = center.x / tileSize; int tiley = center.y / tileSize; int off_x = (center.x % tileSize); int off_y = (center.y % tileSize); int w2 = getWidth() / 2; int h2 = getHeight() / 2; int topLeftX = center.x - w2; int topLeftY = center.y - h2; int posx = w2 - off_x; int posy = h2 - off_y; int diff_left = off_x; int diff_right = tileSize - off_x; int diff_top = off_y; int diff_bottom = tileSize - 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 = -tileSize; int y_min = -tileSize; int x_max = getWidth(); int y_max = getHeight(); // paint the tiles in a spiral, starting from center of the map boolean painted = (mapTileLayers.size() > 0); for (MapTileLayer l : mapTileLayers) { l.startPainting(mapSource); } 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 painted = true; for (MapTileLayer l : mapTileLayers) { l.paintTile(g, posx, posy, tilex, tiley, zoom); } } Point p = move[iMove]; posx += p.x * tileSize; posy += p.y * tileSize; tilex += p.x; tiley += p.y; } iMove = (iMove + 1) % move.length; } } int bottomRightX = topLeftX + getWidth(); int bottomRightY = topLeftY + getHeight(); try { for (MapLayer l : mapLayers) { l.paint(this, (Graphics2D) g, zoom, topLeftX, topLeftY, bottomRightX, bottomRightY); } } catch (ConcurrentModificationException e) { // This may happen when multiple GPX files are loaded at once and in the mean time the map view is // repainted. SwingUtilities.invokeLater(new Runnable() { public void run() { JMapViewer.this.repaint(); } }); } // outer border of the map int mapSize = tileSize << zoom; g.setColor(Color.BLACK); g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize); // g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20); } /** * 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 > mapSource.getMaxZoom() || zoom < mapSource.getMinZoom() || zoom == this.zoom) return; Point2D.Double zoomPos = getPosition(mapPoint); jobDispatcher.cancelOutstandingJobs(); // Clearing outstanding load // requests setDisplayPositionByLatLon(mapPoint, zoomPos.x, zoomPos.y, zoom); } public void setZoom(int zoom) { setZoom(zoom, new Point(getWidth() / 2, getHeight() / 2)); repaint(); } /** * 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 > mapSource.getMinZoom()); zoomInButton.setEnabled(zoom < mapSource.getMaxZoom()); } public boolean isTileGridVisible() { return (mapGridLayer != null); } public void setTileGridVisible(boolean tileGridVisible) { if (isTileGridVisible() == tileGridVisible) return; if (tileGridVisible) { mapGridLayer = new MapGridLayer(); addMapTileLayers(mapGridLayer); } else { removeMapTileLayers(mapGridLayer); mapGridLayer = null; } repaint(); } public boolean getMapMarkersVisible() { return mapMarkersVisible; } public void setZoomContolsVisible(boolean visible) { zoomSlider.setVisible(visible); zoomInButton.setVisible(visible); zoomOutButton.setVisible(visible); } public boolean getZoomContolsVisible() { return zoomSlider.isVisible(); } public MemoryTileCache getTileImageCache() { return tileCache; } public TileLoader getTileLoader() { return tileLoader; } public MapSource getMapSource() { return mapSource; } public void setMapSource(MapSource mapSource) { if (mapSource.getMaxZoom() > MAX_ZOOM) throw new RuntimeException("Maximum zoom level too high"); if (mapSource.getMinZoom() < MIN_ZOOM) throw new RuntimeException("Minumim zoom level too low"); this.mapSource = mapSource; zoomSlider.setMinimum(mapSource.getMinZoom()); zoomSlider.setMaximum(mapSource.getMaxZoom()); jobDispatcher.cancelOutstandingJobs(); if (zoom > mapSource.getMaxZoom()) setZoom(mapSource.getMaxZoom()); mapTileLayers.clear(); log.info("Map layer changed to: " + mapSource); mapTileLayers.add(new DefaultMapTileLayer(this, mapSource)); if (mapGridLayer != null) mapTileLayers.add(mapGridLayer); repaint(); } public JobDispatcher getJobDispatcher() { return jobDispatcher; } public boolean isUsePlaceHolderTiles() { return usePlaceHolderTiles; } public void tileLoadingFinished(Tile tile, boolean success) { repaint(); } public void addMapTileLayers(MapTileLayer mapTileLayer) { mapTileLayers.add(mapTileLayer); } public void removeMapTileLayers(MapTileLayer mapTileLayer) { mapTileLayers.remove(mapTileLayer); } }