From b7d843338f47b3d4102af1d37f21e2e52214d1b2 Mon Sep 17 00:00:00 2001 From: "antoine.gagneux" Date: Tue, 21 Feb 2023 17:04:04 +0100 Subject: [PATCH] Premier Depot --- .classpath | 10 + .gitignore | 1 + .project | 17 + .settings/org.eclipse.jdt.core.prefs | 12 + region-auvergne-rhone-alpes_logo.png | Bin 0 -> 7328 bytes src/ClassePrincipale.java | 8 + src/MaJFrameDemo.java | 192 +++++ src/MaJFrameSimple.java | 35 + .../gui/jmapviewer/Coordinate.java | 55 ++ .../gui/jmapviewer/DefaultMapController.java | 180 ++++ .../gui/jmapviewer/JMapController.java | 35 + .../gui/jmapviewer/JMapViewer.java | 790 ++++++++++++++++++ .../gui/jmapviewer/JobDispatcher.java | 133 +++ .../gui/jmapviewer/MapMarkerDot.java | 60 ++ .../gui/jmapviewer/MapMarkerImage.java | 66 ++ .../gui/jmapviewer/MapMarkerLabel.java | 62 ++ .../gui/jmapviewer/MemoryTileCache.java | 224 +++++ .../jmapviewer/OsmFileCacheTileLoader.java | 457 ++++++++++ .../gui/jmapviewer/OsmMercator.java | 136 +++ .../gui/jmapviewer/OsmTileLoader.java | 113 +++ .../openstreetmap/gui/jmapviewer/Tile.java | 305 +++++++ .../gui/jmapviewer/TileController.java | 84 ++ .../gui/jmapviewer/images/bing_maps.png | Bin 0 -> 4295 bytes .../gui/jmapviewer/images/error.png | Bin 0 -> 5668 bytes .../gui/jmapviewer/images/hourglass.png | Bin 0 -> 9096 bytes .../gui/jmapviewer/images/minus.png | Bin 0 -> 171 bytes .../gui/jmapviewer/images/plus.png | Bin 0 -> 225 bytes .../gui/jmapviewer/interfaces/MapMarker.java | 37 + .../jmapviewer/interfaces/MapRectangle.java | 39 + .../gui/jmapviewer/interfaces/TileCache.java | 45 + .../gui/jmapviewer/interfaces/TileLoader.java | 26 + .../interfaces/TileLoaderListener.java | 18 + .../gui/jmapviewer/interfaces/TileSource.java | 134 +++ .../openstreetmap/gui/jmapviewer/package.html | 14 + .../tilesources/AbstractOsmTileSource.java | 114 +++ .../tilesources/OfflineOsmTileSource.java | 29 + .../jmapviewer/tilesources/OsmTileSource.java | 83 ++ .../jmapviewer/tilesources/TMSTileSource.java | 20 + .../tilesources/TemplatedTMSTileSource.java | 28 + 39 files changed, 3562 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 region-auvergne-rhone-alpes_logo.png create mode 100644 src/ClassePrincipale.java create mode 100644 src/MaJFrameDemo.java create mode 100644 src/MaJFrameSimple.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/Coordinate.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/JMapController.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/JMapViewer.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/JobDispatcher.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/MapMarkerImage.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/MapMarkerLabel.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/OsmFileCacheTileLoader.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/OsmMercator.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/Tile.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/TileController.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/images/bing_maps.png create mode 100644 src/org/openstreetmap/gui/jmapviewer/images/error.png create mode 100644 src/org/openstreetmap/gui/jmapviewer/images/hourglass.png create mode 100644 src/org/openstreetmap/gui/jmapviewer/images/minus.png create mode 100644 src/org/openstreetmap/gui/jmapviewer/images/plus.png create mode 100644 src/org/openstreetmap/gui/jmapviewer/interfaces/MapMarker.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoaderListener.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/package.html create mode 100644 src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/tilesources/OfflineOsmTileSource.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java create mode 100644 src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..2197894 --- /dev/null +++ b/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae3c172 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/.project b/.project new file mode 100644 index 0000000..b42a344 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + ExempleJPanelOSM + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..3fd6c25 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/region-auvergne-rhone-alpes_logo.png b/region-auvergne-rhone-alpes_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1d90fd39cb1b14f3680f1c4f4b7d59dbd2d69478 GIT binary patch literal 7328 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 list = new ArrayList<>(map.getMapMarkerList()); + Iterator it = list.iterator(); + while (it.hasNext()) { + map.removeMapMarker(it.next()); + } + } + + private void addMarkerLabel(int nbMarkers) { + Random r = new Random(); + for (int i=0;ilat and lon. + * + * @author Jan Peter Stotz + * + */ +public class Coordinate implements Serializable { + private transient Point2D.Double data; + + public Coordinate(double lat, double lon) { + data = new Point2D.Double(lon, lat); + } + + public double getLat() { + return data.y; + } + + public void setLat(double lat) { + data.y = lat; + } + + public double getLon() { + return data.x; + } + + public void setLon(double lon) { + data.x = lon; + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeObject(data.x); + out.writeObject(data.y); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + data = new Point2D.Double(); + data.x = (Double) in.readObject(); + data.y = (Double) in.readObject(); + } + + public String toString() { + return "Coordinate[" + data.y + ", " + data.x + "]"; + } +} diff --git a/src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java b/src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java new file mode 100644 index 0000000..81b2e11 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java @@ -0,0 +1,180 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +/** + * Default map controller which implements map moving by pressing the right + * mouse button and zooming by double click or by mouse wheel. + * + * @author Jan Peter Stotz + * + */ +public class DefaultMapController extends JMapController implements MouseListener, MouseMotionListener, +MouseWheelListener { + + private static final int MOUSE_BUTTONS_MASK = MouseEvent.BUTTON3_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK + | MouseEvent.BUTTON2_DOWN_MASK; + + private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; + public DefaultMapController(JMapViewer map) { + super(map); + } + + private Point lastDragPoint; + + private boolean isMoving = false; + + private boolean movementEnabled = true; + + private int movementMouseButton = MouseEvent.BUTTON3; + private int movementMouseButtonMask = MouseEvent.BUTTON3_DOWN_MASK; + + private boolean wheelZoomEnabled = true; + private boolean doubleClickZoomEnabled = true; + + public void mouseDragged(MouseEvent e) { + if (!movementEnabled || !isMoving) + return; + // Is only the selected mouse button pressed? + if ((e.getModifiersEx() & MOUSE_BUTTONS_MASK) == movementMouseButtonMask) { + Point p = e.getPoint(); + if (lastDragPoint != null) { + int diffx = lastDragPoint.x - p.x; + int diffy = lastDragPoint.y - p.y; + map.moveMap(diffx, diffy); + } + lastDragPoint = p; + } + } + + public void mouseClicked(MouseEvent e) { + if (doubleClickZoomEnabled && e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) { + map.zoomIn(e.getPoint()); + } + } + + public void mousePressed(MouseEvent e) { + if (e.getButton() == movementMouseButton || isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK) { + lastDragPoint = null; + isMoving = true; + } + } + + public void mouseReleased(MouseEvent e) { + if (e.getButton() == movementMouseButton || isPlatformOsx() && e.getButton() == MouseEvent.BUTTON1) { + lastDragPoint = null; + isMoving = false; + } + } + + public void mouseWheelMoved(MouseWheelEvent e) { + if (wheelZoomEnabled) { + map.setZoom(map.getZoom() - e.getWheelRotation(), e.getPoint()); + } + } + + public boolean isMovementEnabled() { + return movementEnabled; + } + + /** + * Enables or disables that the map pane can be moved using the mouse. + * + * @param movementEnabled + */ + public void setMovementEnabled(boolean movementEnabled) { + this.movementEnabled = movementEnabled; + } + + public int getMovementMouseButton() { + return movementMouseButton; + } + + /** + * Sets the mouse button that is used for moving the map. Possible values + * are: + *
    + *
  • {@link MouseEvent#BUTTON1} (left mouse button)
  • + *
  • {@link MouseEvent#BUTTON2} (middle mouse button)
  • + *
  • {@link MouseEvent#BUTTON3} (right mouse button)
  • + *
+ * + * @param movementMouseButton + */ + public void setMovementMouseButton(int movementMouseButton) { + this.movementMouseButton = movementMouseButton; + switch (movementMouseButton) { + case MouseEvent.BUTTON1: + movementMouseButtonMask = MouseEvent.BUTTON1_DOWN_MASK; + break; + case MouseEvent.BUTTON2: + movementMouseButtonMask = MouseEvent.BUTTON2_DOWN_MASK; + break; + case MouseEvent.BUTTON3: + movementMouseButtonMask = MouseEvent.BUTTON3_DOWN_MASK; + break; + default: + throw new RuntimeException("Unsupported button"); + } + } + + public boolean isWheelZoomEnabled() { + return wheelZoomEnabled; + } + + public void setWheelZoomEnabled(boolean wheelZoomEnabled) { + this.wheelZoomEnabled = wheelZoomEnabled; + } + + public boolean isDoubleClickZoomEnabled() { + return doubleClickZoomEnabled; + } + + public void setDoubleClickZoomEnabled(boolean doubleClickZoomEnabled) { + this.doubleClickZoomEnabled = doubleClickZoomEnabled; + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mouseMoved(MouseEvent e) { + // Mac OSX simulates with ctrl + mouse 1 the second mouse button hence no dragging events get fired. + // + if (isPlatformOsx()) { + if (!movementEnabled || !isMoving) + return; + // Is only the selected mouse button pressed? + if (e.getModifiersEx() == MouseEvent.CTRL_DOWN_MASK) { + Point p = e.getPoint(); + if (lastDragPoint != null) { + int diffx = lastDragPoint.x - p.x; + int diffy = lastDragPoint.y - p.y; + map.moveMap(diffx, diffy); + } + lastDragPoint = p; + } + + } + + } + + /** + * Replies true if we are currently running on OSX + * + * @return true if we are currently running on OSX + */ + public static boolean isPlatformOsx() { + String os = System.getProperty("os.name"); + return os != null && os.toLowerCase().startsWith("mac os x"); + } +} diff --git a/src/org/openstreetmap/gui/jmapviewer/JMapController.java b/src/org/openstreetmap/gui/jmapviewer/JMapController.java new file mode 100644 index 0000000..63307f1 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/JMapController.java @@ -0,0 +1,35 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelListener; + +/** + * Abstract base class for all mouse controller implementations. For + * implementing your own controller create a class that derives from this one + * and implements one or more of the following interfaces: + *
    + *
  • {@link MouseListener}
  • + *
  • {@link MouseMotionListener}
  • + *
  • {@link MouseWheelListener}
  • + *
+ * + * @author Jan Peter Stotz + */ +public abstract class JMapController { + + protected JMapViewer map; + + public JMapController(JMapViewer map) { + this.map = map; + if (this instanceof MouseListener) + map.addMouseListener((MouseListener) this); + if (this instanceof MouseWheelListener) + map.addMouseWheelListener((MouseWheelListener) this); + if (this instanceof MouseMotionListener) + map.addMouseMotionListener((MouseMotionListener) this); + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java b/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java new file mode 100644 index 0000000..196047a --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java @@ -0,0 +1,790 @@ +package org.openstreetmap.gui.jmapviewer; + +//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.Image; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.font.TextAttribute; +import java.awt.geom.Rectangle2D; +import java.util.HashMap; +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; +import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource; + +/** + * + * 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 mapMarkerList; + protected List 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; + + private TileSource tileSource; + + // Attribution + private Image attrImage; + private String attrTermsUrl; + public static final Font ATTR_FONT = new Font("Arial", Font.PLAIN, 10); + public static final Font ATTR_LINK_FONT; + + static { + HashMap aUnderline = new HashMap(); + aUnderline.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); + ATTR_LINK_FONT = ATTR_FONT.deriveFont(aUnderline); + } + + /** + * 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(); + tileSource = new OsmTileSource.Mapnik(); + tileController = new TileController(tileSource, tileCache, this); + mapMarkerList = new LinkedList(); + mapRectangleList = new LinkedList(); + mapMarkersVisible = true; + mapRectanglesVisible = true; + tileGridVisible = false; + setLayout(null); + initializeZoomSlider(); + setMinimumSize(new Dimension(tileSource.getTileSize(), tileSource.getTileSize())); + setPreferredSize(new Dimension(400, 400)); + setDisplayPositionByLatLon(50, 9, 3); + //setToolTipText(""); + } + + @Override + public String getToolTipText(MouseEvent event) { + // Point screenPoint = event.getLocationOnScreen(); + // Coordinate c = getPosition(screenPoint); + return super.getToolTipText(event); + } + + 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 + * mapPoint. + * + * @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); + } + + /** + * Calculates the latitude/longitude coordinate of the center of the + * currently displayed map area. + * + * @return latitude / longitude + */ + public Coordinate getPosition() { + double lon = OsmMercator.XToLon(center.x, zoom); + double lat = OsmMercator.YToLat(center.y, zoom); + return new Coordinate(lat, lon); + } + + /** + * 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 Coordinate 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 + * @param mapPointY + * @return + */ + 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 null if the point is not visible + * and checkOutside set to true + */ + 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 null 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 null 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 null if the point is not visible + * and checkOutside set to true + */ + 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 tilesize = tileSource.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 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 = 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, tilesize, tilesize); + } + } + } + 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; + 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) { + paintMarker(g, marker); + } + } + paintAttribution(g); + } + + /** + * Paint a single marker. + */ + protected void paintMarker(Graphics g, MapMarker marker) { + 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 mapMarkerList) { + this.mapMarkerList = mapMarkerList; + repaint(); + } + + public List getMapMarkerList() { + return mapMarkerList; + } + + public void setMapRectangleList(List mapRectangleList) { + this.mapRectangleList = mapRectangleList; + repaint(); + } + + public List getMapRectangleList() { + return mapRectangleList; + } + + public void addMapMarker(MapMarker marker) { + mapMarkerList.add(marker); + repaint(); + } + + public void removeMapMarker(MapMarker marker) { + mapMarkerList.remove(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"); + this.tileSource = tileSource; + tileController.setTileSource(tileSource); + zoomSlider.setMinimum(tileSource.getMinZoom()); + zoomSlider.setMaximum(tileSource.getMaxZoom()); + tileController.cancelOutstandingJobs(); + if (zoom > tileSource.getMaxZoom()) { + setZoom(tileSource.getMaxZoom()); + } + boolean requireAttr = tileSource.requiresAttribution(); + if (requireAttr) { + attrImage = tileSource.getAttributionImage(); + attrTermsUrl = tileSource.getTermsOfUseURL(); + } else { + attrImage = null; + attrTermsUrl = null; + } + 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); + } + + private void paintAttribution(Graphics g) { + if (!tileSource.requiresAttribution()) + return; + // Draw attribution + Font font = g.getFont(); + g.setFont(ATTR_LINK_FONT); + + Rectangle2D termsStringBounds = g.getFontMetrics().getStringBounds("Background Terms of Use", g); + int textHeight = (int) termsStringBounds.getHeight() - 5; + int termsTextY = getHeight() - textHeight; + if (attrTermsUrl != null) { + int x = 2; + int y = getHeight() - textHeight; + g.setColor(Color.black); + g.drawString("Background Terms of Use", x + 1, y + 1); + g.setColor(Color.white); + g.drawString("Background Terms of Use", x, y); + } + + // Draw attribution logo + if (attrImage != null) { + int x = 2; + int height = attrImage.getHeight(null); + int y = termsTextY - height - textHeight - 5; + g.drawImage(attrImage, x, y, null); + } + + g.setFont(ATTR_FONT); + Coordinate topLeft = getPosition(0, 0); + Coordinate bottomRight = getPosition(getWidth(), getHeight()); + String attributionText = tileSource.getAttributionText(zoom, topLeft, bottomRight); + if (attributionText != null) { + Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g); + int x = getWidth() - (int) stringBounds.getWidth(); + int y = getHeight() - textHeight; + g.setColor(Color.black); + g.drawString(attributionText, x + 1, y + 1); + g.setColor(Color.white); + g.drawString(attributionText, x, y); + } + + g.setFont(font); + } +} diff --git a/src/org/openstreetmap/gui/jmapviewer/JobDispatcher.java b/src/org/openstreetmap/gui/jmapviewer/JobDispatcher.java new file mode 100644 index 0000000..88471a8 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/JobDispatcher.java @@ -0,0 +1,133 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * A generic class that processes a list of {@link Runnable} one-by-one using + * one or more {@link Thread}-instances. The number of instances varies between + * 1 and {@link #WORKER_THREAD_MAX_COUNT} (default: 8). If an instance is idle + * more than {@link #WORKER_THREAD_TIMEOUT} seconds (default: 30), the instance + * ends itself. + * + * @author Jan Peter Stotz + */ +public class JobDispatcher { + + private static final JobDispatcher instance = new JobDispatcher(); + + /** + * @return the singelton instance of the {@link JobDispatcher} + */ + public static JobDispatcher getInstance() { + return instance; + } + + private JobDispatcher() { + addWorkerThread().firstThread = true; + } + + protected BlockingQueue jobQueue = new LinkedBlockingQueue(); + + public static int WORKER_THREAD_MAX_COUNT = 8; + + /** + * Specifies the time span in seconds that a worker thread waits for new + * jobs to perform. If the time span has elapsed the worker thread + * terminates itself. Only the first worker thread works differently, it + * ignores the timeout and will never terminate itself. + */ + public static int WORKER_THREAD_TIMEOUT = 30; + + /** + * Total number of worker threads currently idle or active + */ + protected int workerThreadCount = 0; + + /** + * Number of worker threads currently idle + */ + protected int workerThreadIdleCount = 0; + + /** + * Just an id for identifying an worker thread instance + */ + protected int workerThreadId = 0; + + /** + * Removes all jobs from the queue that are currently not being processed. + */ + public void cancelOutstandingJobs() { + jobQueue.clear(); + } + + public void addJob(Runnable job) { + try { + jobQueue.put(job); + if (workerThreadIdleCount == 0 && workerThreadCount < WORKER_THREAD_MAX_COUNT) + addWorkerThread(); + } catch (InterruptedException e) { + } + } + + protected JobThread addWorkerThread() { + JobThread jobThread = new JobThread(++workerThreadId); + synchronized (this) { + workerThreadCount++; + } + jobThread.start(); + return jobThread; + } + + public class JobThread extends Thread { + + Runnable job; + boolean firstThread = false; + + public JobThread(int threadId) { + super("OSMJobThread " + threadId); + setDaemon(true); + job = null; + } + + @Override + public void run() { + executeJobs(); + synchronized (instance) { + workerThreadCount--; + } + } + + protected void executeJobs() { + while (!isInterrupted()) { + try { + synchronized (instance) { + workerThreadIdleCount++; + } + if (firstThread) + job = jobQueue.take(); + else + job = jobQueue.poll(WORKER_THREAD_TIMEOUT, TimeUnit.SECONDS); + } catch (InterruptedException e1) { + return; + } finally { + synchronized (instance) { + workerThreadIdleCount--; + } + } + if (job == null) + return; + try { + job.run(); + job = null; + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java b/src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java new file mode 100644 index 0000000..8c81666 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java @@ -0,0 +1,60 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Point; + +import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; + +/** + * A simple implementation of the {@link MapMarker} interface. Each map marker + * is painted as a circle with a black border line and filled with a specified + * color. + * + * @author Jan Peter Stotz + * + */ +public class MapMarkerDot implements MapMarker { + + private double lat; + private double lon; + private Color color; + private int size; + + public MapMarkerDot(double lat, double lon) { + this(Color.YELLOW, 10, lat, lon); + } + + public MapMarkerDot(Color color, int size, double lat, double lon) { + super(); + this.color = color; + this.lat = lat; + this.lon = lon; + this.size = size; + } + + public double getLat() { + return lat; + } + + public double getLon() { + return lon; + } + + public void paint(Graphics g, Point position) { + g.setColor(color); + g.fillOval(position.x - size/2, position.y - size/2, size, size); + g.setColor(Color.BLACK); + g.drawOval(position.x - size/2, position.y - size/2, size, size); + + + } + + @Override + public String toString() { + return "MapMarker at " + lat + " " + lon; + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/MapMarkerImage.java b/src/org/openstreetmap/gui/jmapviewer/MapMarkerImage.java new file mode 100644 index 0000000..5ac9560 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/MapMarkerImage.java @@ -0,0 +1,66 @@ +package org.openstreetmap.gui.jmapviewer; + +import java.awt.Graphics; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; + +/** + * A simple implementation of the {@link MapMarker} interface. Each map marker + * is painted as a circle with a black border line and filled with a specified + * color. + * + * @author Jan Peter Stotz + * + */ +public class MapMarkerImage implements MapMarker { + + private double lat; + private double lon; + private BufferedImage image; + private int size; + + public MapMarkerImage(double lat, double lon) { + this("region-auvergne-rhone-alpes_logo.png", 10, lat, lon); + } + + public MapMarkerImage(String img, int size, double lat, double lon) { + super(); + this.lat = lat; + this.lon = lon; + this.size = size; + + try { + image = ImageIO.read(new File(img)); + } catch (IOException e) { + e.printStackTrace(); + } + + + } + + public double getLat() { + return lat; + } + + public double getLon() { + return lon; + } + + public void paint(Graphics g, Point position) { + g.drawImage(image, position.x - size/2, position.y - size/2, size, size, null); + + + } + + @Override + public String toString() { + return "MapMarker at " + lat + " " + lon; + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/MapMarkerLabel.java b/src/org/openstreetmap/gui/jmapviewer/MapMarkerLabel.java new file mode 100644 index 0000000..2ab6937 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/MapMarkerLabel.java @@ -0,0 +1,62 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Point; + +import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; + +/** + * A simple implementation of the {@link MapMarker} interface. Each map marker + * is painted as a circle with a black border line and filled with a specified + * color. + * + * @author Jan Peter Stotz + * + */ +public class MapMarkerLabel implements MapMarker { + + private double lat; + private double lon; + private Color color; + private String label; + private int sizeFont; + + public MapMarkerLabel(double lat, double lon) { + this(Color.YELLOW, "Label", 16, lat, lon); + } + + public MapMarkerLabel(Color color, String label, int sizeFont, double lat, double lon) { + super(); + this.color = color; + this.lat = lat; + this.lon = lon; + this.label = label; + this.sizeFont = sizeFont; + } + + public double getLat() { + return lat; + } + + public double getLon() { + return lon; + } + + public void paint(Graphics g, Point position) { + g.setColor(color); + g.setFont(new Font("default", Font.BOLD, sizeFont)); + g.drawString(label, position.x , position.y ); + + + } + + @Override + public String toString() { + return "MapMarker at " + lat + " " + lon; + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java b/src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java new file mode 100644 index 0000000..d7b9544 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java @@ -0,0 +1,224 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.util.Hashtable; +import java.util.logging.Logger; + +import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; +import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; + +/** + * {@link TileCache} implementation that stores all {@link Tile} objects in + * memory up to a certain limit ({@link #getCacheSize()}). If the limit is + * exceeded the least recently used {@link Tile} objects will be deleted. + * + * @author Jan Peter Stotz + */ +public class MemoryTileCache implements TileCache { + + protected static final Logger log = Logger.getLogger(MemoryTileCache.class.getName()); + + /** + * Default cache size + */ + protected int cacheSize = 200; + + protected Hashtable hashtable; + + /** + * List of all tiles in their last recently used order + */ + protected CacheLinkedListElement lruTiles; + + public MemoryTileCache() { + hashtable = new Hashtable(cacheSize); + lruTiles = new CacheLinkedListElement(); + } + + public void addTile(Tile tile) { + CacheEntry entry = createCacheEntry(tile); + hashtable.put(tile.getKey(), entry); + lruTiles.addFirst(entry); + if (hashtable.size() > cacheSize) + removeOldEntries(); + } + + public Tile getTile(TileSource source, int x, int y, int z) { + CacheEntry entry = hashtable.get(Tile.getTileKey(source, x, y, z)); + if (entry == null) + return null; + // We don't care about placeholder tiles and hourglass image tiles, the + // important tiles are the loaded ones + if (entry.tile.isLoaded()) + lruTiles.moveElementToFirstPos(entry); + return entry.tile; + } + + /** + * Removes the least recently used tiles + */ + protected void removeOldEntries() { + synchronized (lruTiles) { + try { + while (lruTiles.getElementCount() > cacheSize) { + removeEntry(lruTiles.getLastElement()); + } + } catch (Exception e) { + log.warning(e.getMessage()); + } + } + } + + protected void removeEntry(CacheEntry entry) { + hashtable.remove(entry.tile.getKey()); + lruTiles.removeEntry(entry); + } + + protected CacheEntry createCacheEntry(Tile tile) { + return new CacheEntry(tile); + } + + /** + * Clears the cache deleting all tiles from memory + */ + public void clear() { + synchronized (lruTiles) { + hashtable.clear(); + lruTiles.clear(); + } + } + + public int getTileCount() { + return hashtable.size(); + } + + public int getCacheSize() { + return cacheSize; + } + + /** + * Changes the maximum number of {@link Tile} objects that this cache holds. + * + * @param cacheSize + * new maximum number of tiles + */ + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + if (hashtable.size() > cacheSize) + removeOldEntries(); + } + + /** + * Linked list element holding the {@link Tile} and links to the + * {@link #next} and {@link #prev} item in the list. + */ + protected static class CacheEntry { + Tile tile; + + CacheEntry next; + CacheEntry prev; + + protected CacheEntry(Tile tile) { + this.tile = tile; + } + + public Tile getTile() { + return tile; + } + + public CacheEntry getNext() { + return next; + } + + public CacheEntry getPrev() { + return prev; + } + + } + + /** + * Special implementation of a double linked list for {@link CacheEntry} + * elements. It supports element removal in constant time - in difference to + * the Java implementation which needs O(n). + * + * @author Jan Peter Stotz + */ + protected static class CacheLinkedListElement { + protected CacheEntry firstElement = null; + protected CacheEntry lastElement; + protected int elementCount; + + public CacheLinkedListElement() { + clear(); + } + + public synchronized void clear() { + elementCount = 0; + firstElement = null; + lastElement = null; + } + + /** + * Add the element to the head of the list. + * + * @param new element to be added + */ + public synchronized void addFirst(CacheEntry element) { + if (elementCount == 0) { + firstElement = element; + lastElement = element; + element.prev = null; + element.next = null; + } else { + element.next = firstElement; + firstElement.prev = element; + element.prev = null; + firstElement = element; + } + elementCount++; + } + + /** + * Removes the specified elemntent form the list. + * + * @param element + * to be removed + */ + public synchronized void removeEntry(CacheEntry element) { + if (element.next != null) { + element.next.prev = element.prev; + } + if (element.prev != null) { + element.prev.next = element.next; + } + if (element == firstElement) + firstElement = element.next; + if (element == lastElement) + lastElement = element.prev; + element.next = null; + element.prev = null; + elementCount--; + } + + public synchronized void moveElementToFirstPos(CacheEntry entry) { + if (firstElement == entry) + return; + removeEntry(entry); + addFirst(entry); + } + + public int getElementCount() { + return elementCount; + } + + public CacheEntry getLastElement() { + return lastElement; + } + + public CacheEntry getFirstElement() { + return firstElement; + } + + } +} diff --git a/src/org/openstreetmap/gui/jmapviewer/OsmFileCacheTileLoader.java b/src/org/openstreetmap/gui/jmapviewer/OsmFileCacheTileLoader.java new file mode 100644 index 0000000..57396ce --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/OsmFileCacheTileLoader.java @@ -0,0 +1,457 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.Charset; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +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; +import org.openstreetmap.gui.jmapviewer.interfaces.TileSource.TileUpdate; + +/** + * A {@link TileLoader} implementation that loads tiles from OSM via HTTP and + * saves all loaded files in a directory located in the the temporary directory. + * If a tile is present in this file cache it will not be loaded from OSM again. + * + * @author Jan Peter Stotz + * @author Stefan Zeller + */ +public class OsmFileCacheTileLoader extends OsmTileLoader { + + private static final Logger log = Logger.getLogger(OsmFileCacheTileLoader.class.getName()); + + private static final String ETAG_FILE_EXT = ".etag"; + private static final String TAGS_FILE_EXT = ".tags"; + + private static final Charset TAGS_CHARSET = Charset.forName("UTF-8"); + + public static final long FILE_AGE_ONE_DAY = 1000 * 60 * 60 * 24; + public static final long FILE_AGE_ONE_WEEK = FILE_AGE_ONE_DAY * 7; + + protected String cacheDirBase; + + protected long maxCacheFileAge = FILE_AGE_ONE_WEEK; + protected long recheckAfter = FILE_AGE_ONE_DAY; + + public static File getDefaultCacheDir() throws SecurityException { + String tempDir = null; + String userName = System.getProperty("user.name"); + try { + tempDir = System.getProperty("java.io.tmpdir"); + } catch (SecurityException e) { + log.log(Level.WARNING, + "Failed to access system property ''java.io.tmpdir'' for security reasons. Exception was: " + + e.toString()); + throw e; // rethrow + } + try { + if (tempDir == null) + throw new IOException("No temp directory set"); + String subDirName = "JMapViewerTiles"; + // On Linux/Unix systems we do not have a per user tmp directory. + // Therefore we add the user name for getting a unique dir name. + if (userName != null && userName.length() > 0) { + subDirName += "_" + userName; + } + File cacheDir = new File(tempDir, subDirName); + return cacheDir; + } catch (Exception e) { + } + return null; + } + + /** + * Create a OSMFileCacheTileLoader with given cache directory. + * If cacheDir is not set or invalid, IOException will be thrown. + * @param map + * @param cacheDir + */ + public OsmFileCacheTileLoader(TileLoaderListener map, File cacheDir) throws IOException { + super(map); + if (cacheDir == null || (!cacheDir.exists() && !cacheDir.mkdirs())) + throw new IOException("Cannot access cache directory"); + + log.finest("Tile cache directory: " + cacheDir); + cacheDirBase = cacheDir.getAbsolutePath(); + } + + /** + * Create a OSMFileCacheTileLoader with system property temp dir. + * If not set an IOException will be thrown. + * @param map + */ + public OsmFileCacheTileLoader(TileLoaderListener map) throws SecurityException, IOException { + this(map, getDefaultCacheDir()); + } + + @Override + public Runnable createTileLoaderJob(final TileSource source, final int tilex, final int tiley, final int zoom) { + return new FileLoadJob(source, tilex, tiley, zoom); + } + + protected class FileLoadJob implements Runnable { + InputStream input = null; + + int tilex, tiley, zoom; + Tile tile; + TileSource source; + File tileCacheDir; + File tileFile = null; + long fileAge = 0; + boolean fileTilePainted = false; + + public FileLoadJob(TileSource source, int tilex, int tiley, int zoom) { + this.source = source; + this.tilex = tilex; + this.tiley = tiley; + this.zoom = zoom; + } + + public void run() { + TileCache cache = listener.getTileCache(); + synchronized (cache) { + tile = cache.getTile(source, tilex, tiley, zoom); + if (tile == null || tile.isLoaded() || tile.loading) + return; + tile.loading = true; + } + tileCacheDir = new File(cacheDirBase, source.getName().replaceAll("[\\\\/:*?\"<>|]", "_")); + if (!tileCacheDir.exists()) { + tileCacheDir.mkdirs(); + } + if (loadTileFromFile()) + return; + if (fileTilePainted) { + Runnable job = new Runnable() { + + public void run() { + loadOrUpdateTile(); + } + }; + JobDispatcher.getInstance().addJob(job); + } else { + loadOrUpdateTile(); + } + } + + protected void loadOrUpdateTile() { + + try { + // log.finest("Loading tile from OSM: " + tile); + URLConnection urlConn = loadTileFromOsm(tile); + if (tileFile != null) { + switch (source.getTileUpdate()) { + case IfModifiedSince: + urlConn.setIfModifiedSince(fileAge); + break; + case LastModified: + if (!isOsmTileNewer(fileAge)) { + log.finest("LastModified test: local version is up to date: " + tile); + tile.setLoaded(true); + tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge + recheckAfter); + return; + } + break; + } + } + if (source.getTileUpdate() == TileUpdate.ETag || source.getTileUpdate() == TileUpdate.IfNoneMatch) { + String fileETag = tile.getValue("etag"); + if (fileETag != null) { + switch (source.getTileUpdate()) { + case IfNoneMatch: + urlConn.addRequestProperty("If-None-Match", fileETag); + break; + case ETag: + if (hasOsmTileETag(fileETag)) { + tile.setLoaded(true); + tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge + + recheckAfter); + return; + } + } + } + tile.putValue("etag", urlConn.getHeaderField("ETag")); + } + if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 304) { + // If we are isModifiedSince or If-None-Match has been set + // and the server answers with a HTTP 304 = "Not Modified" + log.finest("ETag test: local version is up to date: " + tile); + tile.setLoaded(true); + tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge + recheckAfter); + return; + } + + loadTileMetadata(tile, urlConn); + saveTagsToFile(); + + if ("no-tile".equals(tile.getValue("tile-info"))) + { + tile.setError("No tile at this zoom level"); + listener.tileLoadingFinished(tile, true); + } else { + byte[] buffer = loadTileInBuffer(urlConn); + if (buffer != null) { + tile.loadImage(new ByteArrayInputStream(buffer)); + tile.setLoaded(true); + listener.tileLoadingFinished(tile, true); + saveTileToFile(buffer); + } + } + } catch (Exception e) { + tile.setError(e.getMessage()); + listener.tileLoadingFinished(tile, false); + if (input == null) { + System.err.println("failed loading " + zoom + "/" + tilex + "/" + tiley + " " + e.getMessage()); + } + } finally { + tile.loading = false; + tile.setLoaded(true); + } + } + + protected boolean loadTileFromFile() { + FileInputStream fin = null; + try { + tileFile = getTileFile(); + loadTagsFromFile(); + if ("no-tile".equals(tile.getValue("tile-info"))) + { + tile.setError("No tile at this zoom level"); + if (tileFile.exists()) { + tileFile.delete(); + } + tileFile = getTagsFile(); + } else { + fin = new FileInputStream(tileFile); + if (fin.available() == 0) + throw new IOException("File empty"); + tile.loadImage(fin); + fin.close(); + } + + fileAge = tileFile.lastModified(); + boolean oldTile = System.currentTimeMillis() - fileAge > maxCacheFileAge; + // System.out.println("Loaded from file: " + tile); + if (!oldTile) { + tile.setLoaded(true); + listener.tileLoadingFinished(tile, true); + fileTilePainted = true; + return true; + } + listener.tileLoadingFinished(tile, true); + fileTilePainted = true; + } catch (Exception e) { + try { + if (fin != null) { + fin.close(); + tileFile.delete(); + } + } catch (Exception e1) { + } + tileFile = null; + fileAge = 0; + } + return false; + } + + protected byte[] loadTileInBuffer(URLConnection urlConn) throws IOException { + input = urlConn.getInputStream(); + ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available()); + byte[] buffer = new byte[2048]; + boolean finished = false; + do { + int read = input.read(buffer); + if (read >= 0) { + bout.write(buffer, 0, read); + } else { + finished = true; + } + } while (!finished); + if (bout.size() == 0) + return null; + return bout.toByteArray(); + } + + /** + * Performs a HEAD request for retrieving the + * LastModified header value. + * + * Note: This does only work with servers providing the + * LastModified header: + *
    + *
  • {@link OsmTileLoader#MAP_OSMA} - supported
  • + *
  • {@link OsmTileLoader#MAP_MAPNIK} - not supported
  • + *
+ * + * @param fileAge + * @return true if the tile on the server is newer than the + * file + * @throws IOException + */ + protected boolean isOsmTileNewer(long fileAge) throws IOException { + URL url; + url = new URL(tile.getUrl()); + HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); + prepareHttpUrlConnection(urlConn); + urlConn.setRequestMethod("HEAD"); + urlConn.setReadTimeout(30000); // 30 seconds read timeout + // System.out.println("Tile age: " + new + // Date(urlConn.getLastModified()) + " / " + // + new Date(fileAge)); + long lastModified = urlConn.getLastModified(); + if (lastModified == 0) + return true; // no LastModified time returned + return (lastModified > fileAge); + } + + protected boolean hasOsmTileETag(String eTag) throws IOException { + URL url; + url = new URL(tile.getUrl()); + HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); + prepareHttpUrlConnection(urlConn); + urlConn.setRequestMethod("HEAD"); + urlConn.setReadTimeout(30000); // 30 seconds read timeout + // System.out.println("Tile age: " + new + // Date(urlConn.getLastModified()) + " / " + // + new Date(fileAge)); + String osmETag = urlConn.getHeaderField("ETag"); + if (osmETag == null) + return true; + return (osmETag.equals(eTag)); + } + + protected File getTileFile() { + return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "." + + source.getTileType()); + } + + protected File getTagsFile() { + return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + + TAGS_FILE_EXT); + } + + protected void saveTileToFile(byte[] rawData) { + try { + FileOutputStream f = new FileOutputStream(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + + "_" + tile.getYtile() + "." + source.getTileType()); + f.write(rawData); + f.close(); + // System.out.println("Saved tile to file: " + tile); + } catch (Exception e) { + System.err.println("Failed to save tile content: " + e.getLocalizedMessage()); + } + } + + protected void saveTagsToFile() { + File tagsFile = getTagsFile(); + if (tile.getMetadata() == null) { + tagsFile.delete(); + return; + } + try { + final PrintWriter f = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tagsFile), + TAGS_CHARSET)); + for (Entry entry : tile.getMetadata().entrySet()) { + f.println(entry.getKey() + "=" + entry.getValue()); + } + f.close(); + } catch (Exception e) { + System.err.println("Failed to save tile tags: " + e.getLocalizedMessage()); + } + } + + /** Load backward-compatiblity .etag file and if it exists move it to new .tags file*/ + private void loadOldETagfromFile() { + File etagFile = new File(tileCacheDir, tile.getZoom() + "_" + + tile.getXtile() + "_" + tile.getYtile() + ETAG_FILE_EXT); + if (!etagFile.exists()) return; + try { + FileInputStream f = new FileInputStream(etagFile); + byte[] buf = new byte[f.available()]; + f.read(buf); + f.close(); + String etag = new String(buf, TAGS_CHARSET.name()); + tile.putValue("etag", etag); + if (etagFile.delete()) { + saveTagsToFile(); + } + } catch (IOException e) { + System.err.println("Failed to load compatiblity etag: " + e.getLocalizedMessage()); + } + } + + protected void loadTagsFromFile() { + loadOldETagfromFile(); + File tagsFile = getTagsFile(); + try { + final BufferedReader f = new BufferedReader(new InputStreamReader(new FileInputStream(tagsFile), + TAGS_CHARSET)); + for (String line = f.readLine(); line != null; line = f.readLine()) { + final int i = line.indexOf('='); + if (i == -1 || i == 0) { + System.err.println("Malformed tile tag in file '" + tagsFile.getName() + "':" + line); + continue; + } + tile.putValue(line.substring(0,i),line.substring(i+1)); + } + f.close(); + } catch (FileNotFoundException e) { + } catch (Exception e) { + System.err.println("Failed to load tile tags: " + e.getLocalizedMessage()); + } + } + + } + + public long getMaxFileAge() { + return maxCacheFileAge; + } + + /** + * Sets the maximum age of the local cached tile in the file system. If a + * local tile is older than the specified file age + * {@link OsmFileCacheTileLoader} will connect to the tile server and check + * if a newer tile is available using the mechanism specified for the + * selected tile source/server. + * + * @param maxFileAge + * maximum age in milliseconds + * @see #FILE_AGE_ONE_DAY + * @see #FILE_AGE_ONE_WEEK + * @see TileSource#getTileUpdate() + */ + public void setCacheMaxFileAge(long maxFileAge) { + this.maxCacheFileAge = maxFileAge; + } + + public String getCacheDirBase() { + return cacheDirBase; + } + + public void setTileCacheDir(String tileCacheDir) { + File dir = new File(tileCacheDir); + dir.mkdirs(); + this.cacheDirBase = dir.getAbsolutePath(); + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java b/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java new file mode 100644 index 0000000..4a1f0a4 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java @@ -0,0 +1,136 @@ +package org.openstreetmap.gui.jmapviewer; + +// License: GPL. Copyright 2007 by Tim Haussmann + +/** + * This class implements the Mercator Projection as it is used by Openstreetmap + * (and google). It provides methods to translate coordinates from 'map space' + * into latitude and longitude (on the WGS84 ellipsoid) and vice versa. Map + * space is measured in pixels. The origin of the map space is the top left + * corner. The map space origin (0,0) has latitude ~85 and longitude -180 + * + * @author Tim Haussmann + * + */ + +public class OsmMercator { + + private static int TILE_SIZE = 256; + public static final double MAX_LAT = 85.05112877980659; + public static final double MIN_LAT = -85.05112877980659; + + public static double radius(int aZoomlevel) { + return (TILE_SIZE * (1 << aZoomlevel)) / (2.0 * Math.PI); + } + + /** + * Returns the absolut number of pixels in y or x, defined as: 2^Zoomlevel * + * TILE_WIDTH where TILE_WIDTH is the width of a tile in pixels + * + * @param aZoomlevel + * @return + */ + public static int getMaxPixels(int aZoomlevel) { + return TILE_SIZE * (1 << aZoomlevel); + } + + public static int falseEasting(int aZoomlevel) { + return getMaxPixels(aZoomlevel) / 2; + } + + public static int falseNorthing(int aZoomlevel) { + return (-1 * getMaxPixels(aZoomlevel) / 2); + } + + /** + * Transform longitude to pixelspace + * + *

+ * Mathematical optimization
+ * + * x = radius(aZoomlevel) * toRadians(aLongitude) + falseEasting(aZoomLevel)
+ * x = getMaxPixels(aZoomlevel) / (2 * PI) * (aLongitude * PI) / 180 + getMaxPixels(aZoomlevel) / 2
+ * x = getMaxPixels(aZoomlevel) * aLongitude / 360 + 180 * getMaxPixels(aZoomlevel) / 360
+ * x = getMaxPixels(aZoomlevel) * (aLongitude + 180) / 360
+ *
+ *

+ * + * @param aLongitude + * [-180..180] + * @return [0..2^Zoomlevel*TILE_SIZE[ + * @author Jan Peter Stotz + */ + public static int LonToX(double aLongitude, int aZoomlevel) { + int mp = getMaxPixels(aZoomlevel); + int x = (int) ((mp * (aLongitude + 180l)) / 360l); + x = Math.min(x, mp - 1); + return x; + } + + /** + * Transforms latitude to pixelspace + *

+ * Mathematical optimization
+ * + * log(u) := log((1.0 + sin(toRadians(aLat))) / (1.0 - sin(toRadians(aLat))
+ * + * y = -1 * (radius(aZoomlevel) / 2 * log(u)))) - falseNorthing(aZoomlevel))
+ * y = -1 * (getMaxPixel(aZoomlevel) / 2 * PI / 2 * log(u)) - -1 * getMaxPixel(aZoomLevel) / 2
+ * y = getMaxPixel(aZoomlevel) / (-4 * PI) * log(u)) + getMaxPixel(aZoomLevel) / 2
+ * y = getMaxPixel(aZoomlevel) * ((log(u) / (-4 * PI)) + 1/2)
+ *
+ *

+ * @param aLat + * [-90...90] + * @return [0..2^Zoomlevel*TILE_SIZE[ + * @author Jan Peter Stotz + */ + public static int LatToY(double aLat, int aZoomlevel) { + if (aLat < MIN_LAT) + aLat = MIN_LAT; + else if (aLat > MAX_LAT) + aLat = MAX_LAT; + double sinLat = Math.sin(Math.toRadians(aLat)); + double log = Math.log((1.0 + sinLat) / (1.0 - sinLat)); + int mp = getMaxPixels(aZoomlevel); + int y = (int) (mp * (0.5 - (log / (4.0 * Math.PI)))); + y = Math.min(y, mp - 1); + return y; + } + + /** + * Transforms pixel coordinate X to longitude + * + *

+ * Mathematical optimization
+ * + * lon = toDegree((aX - falseEasting(aZoomlevel)) / radius(aZoomlevel))
+ * lon = 180 / PI * ((aX - getMaxPixels(aZoomlevel) / 2) / getMaxPixels(aZoomlevel) / (2 * PI)
+ * lon = 180 * ((aX - getMaxPixels(aZoomlevel) / 2) / getMaxPixels(aZoomlevel))
+ * lon = 360 / getMaxPixels(aZoomlevel) * (aX - getMaxPixels(aZoomlevel) / 2)
+ * lon = 360 * aX / getMaxPixels(aZoomlevel) - 180
+ *
+ *

+ * @param aX + * [0..2^Zoomlevel*TILE_WIDTH[ + * @return ]-180..180[ + * @author Jan Peter Stotz + */ + public static double XToLon(int aX, int aZoomlevel) { + return ((360d * aX) / getMaxPixels(aZoomlevel)) - 180.0; + } + + /** + * Transforms pixel coordinate Y to latitude + * + * @param aY + * [0..2^Zoomlevel*TILE_WIDTH[ + * @return [MIN_LAT..MAX_LAT] is about [-85..85] + */ + public static double YToLat(int aY, int aZoomlevel) { + aY += falseNorthing(aZoomlevel); + double latitude = (Math.PI / 2) - (2 * Math.atan(Math.exp(-1.0 * aY / radius(aZoomlevel)))); + return -1 * Math.toDegrees(latitude); + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java b/src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java new file mode 100644 index 0000000..b8abbe3 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java @@ -0,0 +1,113 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; + +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; + +/** + * A {@link TileLoader} implementation that loads tiles from OSM. + * + * @author Jan Peter Stotz + */ +public class OsmTileLoader implements TileLoader { + + /** + * Holds the used user agent used for HTTP requests. If this field is + * null, the default Java user agent is used. + */ + public static String USER_AGENT = null; + public static String ACCEPT = "text/html, image/png, image/jpeg, image/gif, */*"; + + protected TileLoaderListener listener; + + public OsmTileLoader(TileLoaderListener listener) { + this.listener = listener; + } + + public Runnable createTileLoaderJob(final TileSource source, final int tilex, final int tiley, final int zoom) { + return new Runnable() { + + InputStream input = null; + + public void run() { + TileCache cache = listener.getTileCache(); + Tile tile; + synchronized (cache) { + tile = cache.getTile(source, tilex, tiley, zoom); + if (tile == null || tile.isLoaded() || tile.loading) + return; + tile.loading = true; + } + try { + // Thread.sleep(500); + URLConnection conn = loadTileFromOsm(tile); + loadTileMetadata(tile, conn); + if ("no-tile".equals(tile.getValue("tile-info"))) { + tile.setError("No tile at this zoom level"); + } else { + input = conn.getInputStream(); + tile.loadImage(input); + input.close(); + input = null; + } + tile.setLoaded(true); + listener.tileLoadingFinished(tile, true); + } catch (Exception e) { + tile.setError(e.getMessage()); + listener.tileLoadingFinished(tile, false); + if (input == null) { + System.err.println("failed loading " + zoom + "/" + tilex + "/" + tiley + " " + e.getMessage()); + } + } finally { + tile.loading = false; + tile.setLoaded(true); + } + } + + }; + } + + protected URLConnection loadTileFromOsm(Tile tile) throws IOException { + URL url; + url = new URL(tile.getUrl()); + URLConnection urlConn = url.openConnection(); + if (urlConn instanceof HttpURLConnection) { + prepareHttpUrlConnection((HttpURLConnection)urlConn); + } + urlConn.setReadTimeout(30000); // 30 seconds read timeout + return urlConn; + } + + protected void loadTileMetadata(Tile tile, URLConnection urlConn) { + String str = urlConn.getHeaderField("X-VE-TILEMETA-CaptureDatesRange"); + if (str != null) { + tile.putValue("capture-date", str); + } + str = urlConn.getHeaderField("X-VE-Tile-Info"); + if (str != null) { + tile.putValue("tile-info", str); + } + } + + protected void prepareHttpUrlConnection(HttpURLConnection urlConn) { + if (USER_AGENT != null) { + urlConn.setRequestProperty("User-agent", USER_AGENT); + } + urlConn.setRequestProperty("Accept", ACCEPT); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/Tile.java b/src/org/openstreetmap/gui/jmapviewer/Tile.java new file mode 100644 index 0000000..e4faf8a --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/Tile.java @@ -0,0 +1,305 @@ +package org.openstreetmap.gui.jmapviewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import javax.imageio.ImageIO; + +import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; +import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; + +/** + * Holds one map tile. Additionally the code for loading the tile image and + * painting it is also included in this class. + * + * @author Jan Peter Stotz + */ +public class Tile { + + /** + * Hourglass image that is displayed until a map tile has been loaded + */ + public static BufferedImage LOADING_IMAGE; + public static BufferedImage ERROR_IMAGE; + + static { + try { + LOADING_IMAGE = ImageIO.read(JMapViewer.class.getResourceAsStream("images/hourglass.png")); + ERROR_IMAGE = ImageIO.read(JMapViewer.class.getResourceAsStream("images/error.png")); + } catch (Exception e1) { + LOADING_IMAGE = null; + ERROR_IMAGE = null; + } + } + + protected TileSource source; + protected int xtile; + protected int ytile; + protected int zoom; + protected BufferedImage image; + protected String key; + protected boolean loaded = false; + protected boolean loading = false; + protected boolean error = false; + protected String error_message; + + /** TileLoader-specific tile metadata */ + protected Map metadata; + + /** + * Creates a tile with empty image. + * + * @param source + * @param xtile + * @param ytile + * @param zoom + */ + public Tile(TileSource source, int xtile, int ytile, int zoom) { + super(); + this.source = source; + this.xtile = xtile; + this.ytile = ytile; + this.zoom = zoom; + this.image = LOADING_IMAGE; + this.key = getTileKey(source, xtile, ytile, zoom); + } + + public Tile(TileSource source, int xtile, int ytile, int zoom, BufferedImage image) { + this(source, xtile, ytile, zoom); + this.image = image; + } + + /** + * Tries to get tiles of a lower or higher zoom level (one or two level + * difference) from cache and use it as a placeholder until the tile has + * been loaded. + */ + public void loadPlaceholderFromCache(TileCache cache) { + BufferedImage tmpImage = new BufferedImage(source.getTileSize(), source.getTileSize(), BufferedImage.TYPE_INT_RGB); + Graphics2D g = (Graphics2D) tmpImage.getGraphics(); + // g.drawImage(image, 0, 0, null); + for (int zoomDiff = 1; zoomDiff < 5; zoomDiff++) { + // first we check if there are already the 2^x tiles + // of a higher detail level + int zoom_high = zoom + zoomDiff; + if (zoomDiff < 3 && zoom_high <= JMapViewer.MAX_ZOOM) { + int factor = 1 << zoomDiff; + int xtile_high = xtile << zoomDiff; + int ytile_high = ytile << zoomDiff; + double scale = 1.0 / factor; + g.setTransform(AffineTransform.getScaleInstance(scale, scale)); + int paintedTileCount = 0; + for (int x = 0; x < factor; x++) { + for (int y = 0; y < factor; y++) { + Tile tile = cache.getTile(source, xtile_high + x, ytile_high + y, zoom_high); + if (tile != null && tile.isLoaded()) { + paintedTileCount++; + tile.paint(g, x * source.getTileSize(), y * source.getTileSize()); + } + } + } + if (paintedTileCount == factor * factor) { + image = tmpImage; + return; + } + } + + int zoom_low = zoom - zoomDiff; + if (zoom_low >= JMapViewer.MIN_ZOOM) { + int xtile_low = xtile >> zoomDiff; + int ytile_low = ytile >> zoomDiff; + int factor = (1 << zoomDiff); + double scale = factor; + AffineTransform at = new AffineTransform(); + int translate_x = (xtile % factor) * source.getTileSize(); + int translate_y = (ytile % factor) * source.getTileSize(); + at.setTransform(scale, 0, 0, scale, -translate_x, -translate_y); + g.setTransform(at); + Tile tile = cache.getTile(source, xtile_low, ytile_low, zoom_low); + if (tile != null && tile.isLoaded()) { + tile.paint(g, 0, 0); + image = tmpImage; + return; + } + } + } + } + + public TileSource getSource() { + return source; + } + + /** + * @return tile number on the x axis of this tile + */ + public int getXtile() { + return xtile; + } + + /** + * @return tile number on the y axis of this tile + */ + public int getYtile() { + return ytile; + } + + /** + * @return zoom level of this tile + */ + public int getZoom() { + return zoom; + } + + public BufferedImage getImage() { + return image; + } + + public void setImage(BufferedImage image) { + this.image = image; + } + + public void loadImage(InputStream input) throws IOException { + image = ImageIO.read(input); + } + + /** + * @return key that identifies a tile + */ + public String getKey() { + return key; + } + + public boolean isLoaded() { + return loaded; + } + + public boolean isLoading() { + return loading; + } + + public void setLoaded(boolean loaded) { + this.loaded = loaded; + } + + public String getUrl() throws IOException { + return source.getTileUrl(zoom, xtile, ytile); + } + + /** + * Paints the tile-image on the {@link Graphics} g at the + * position x/y. + * + * @param g + * @param x + * x-coordinate in g + * @param y + * y-coordinate in g + */ + public void paint(Graphics g, int x, int y) { + if (image == null) + return; + g.drawImage(image, x, y, null); + } + + @Override + public String toString() { + return "Tile " + key; + } + + /** + * Note that the hash code does not include the {@link #source}. + * Therefore a hash based collection can only contain tiles + * of one {@link #source}. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + xtile; + result = prime * result + ytile; + result = prime * result + zoom; + return result; + } + + /** + * Compares this object with obj based on + * the fields {@link #xtile}, {@link #ytile} and + * {@link #zoom}. + * The {@link #source} field is ignored. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Tile other = (Tile) obj; + if (xtile != other.xtile) + return false; + if (ytile != other.ytile) + return false; + if (zoom != other.zoom) + return false; + return true; + } + + public static String getTileKey(TileSource source, int xtile, int ytile, int zoom) { + return zoom + "/" + xtile + "/" + ytile + "@" + source.getName(); + } + + public String getStatus() { + if (this.error) + return "error"; + if (this.loaded) + return "loaded"; + if (this.loading) + return "loading"; + return "new"; + } + + public boolean hasError() { + return error; + } + + public String getErrorMessage() { + return error_message; + } + + public void setError(String message) { + error = true; + setImage(ERROR_IMAGE); + error_message = message; + } + + public void putValue(String key, String value) { + if (value == null || "".equals(value)) { + if (metadata != null) { + metadata.remove(key); + } + return; + } + if (metadata == null) { + metadata = new HashMap(); + } + metadata.put(key, value); + } + + public String getValue(String key) { + if (metadata == null) return null; + return metadata.get(key); + } + + public Map getMetadata() { + return metadata; + } +} diff --git a/src/org/openstreetmap/gui/jmapviewer/TileController.java b/src/org/openstreetmap/gui/jmapviewer/TileController.java new file mode 100644 index 0000000..0dc4c69 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/TileController.java @@ -0,0 +1,84 @@ +package org.openstreetmap.gui.jmapviewer; + +import org.openstreetmap.gui.jmapviewer.JobDispatcher.JobThread; +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; +import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource; + +public class TileController { + protected TileLoader tileLoader; + protected TileCache tileCache; + protected TileSource tileSource; + + JobDispatcher jobDispatcher; + + public TileController(TileSource source, TileCache tileCache, TileLoaderListener listener) { + tileSource = new OsmTileSource.Mapnik(); + tileLoader = new OsmTileLoader(listener); + this.tileCache = tileCache; + jobDispatcher = JobDispatcher.getInstance(); + } + + /** + * retrieves a tile from the cache. If the tile is not present in the cache + * a load job is added to the working queue of {@link JobThread}. + * + * @param tilex + * @param tiley + * @param zoom + * @return specified tile from the cache or null if the tile + * was not found in the cache. + */ + public Tile getTile(int tilex, int tiley, int zoom) { + int max = (1 << zoom); + if (tilex < 0 || tilex >= max || tiley < 0 || tiley >= max) + return null; + Tile tile = tileCache.getTile(tileSource, tilex, tiley, zoom); + if (tile == null) { + tile = new Tile(tileSource, tilex, tiley, zoom); + tileCache.addTile(tile); + tile.loadPlaceholderFromCache(tileCache); + } + if (!tile.isLoaded()) { + jobDispatcher.addJob(tileLoader.createTileLoaderJob(tileSource, tilex, tiley, zoom)); + } + return tile; + } + + public TileCache getTileCache() { + return tileCache; + } + + public void setTileCache(TileCache tileCache) { + this.tileCache = tileCache; + } + + public TileLoader getTileLoader() { + return tileLoader; + } + + public void setTileLoader(TileLoader tileLoader) { + this.tileLoader = tileLoader; + } + + public TileSource getTileLayerSource() { + return tileSource; + } + + public TileSource getTileSource() { + return tileSource; + } + + public void setTileSource(TileSource tileSource) { + this.tileSource = tileSource; + } + + /** + * + */ + public void cancelOutstandingJobs() { + jobDispatcher.cancelOutstandingJobs(); + } +} diff --git a/src/org/openstreetmap/gui/jmapviewer/images/bing_maps.png b/src/org/openstreetmap/gui/jmapviewer/images/bing_maps.png new file mode 100644 index 0000000000000000000000000000000000000000..ae4367e87700710a625651348ea2f214d31b1788 GIT binary patch literal 4295 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 literal 0 HcmV?d00001 diff --git a/src/org/openstreetmap/gui/jmapviewer/images/error.png b/src/org/openstreetmap/gui/jmapviewer/images/error.png new file mode 100644 index 0000000000000000000000000000000000000000..c6a4b4bf561ef7bf6fd57b3bbb260da3fe7eb335 GIT binary patch literal 5668 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 E1GWnM_5c6? literal 0 HcmV?d00001 diff --git a/src/org/openstreetmap/gui/jmapviewer/images/minus.png b/src/org/openstreetmap/gui/jmapviewer/images/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..4093e64277e30f6d908045cfea3eec9d1dfdcc3b GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kmUKs7M+SzC{oH>NS%G}c0*}aI z1_mZ85N7;mrWptnlqhkHC<)F_D=AMbN@WO0%*-p%^K%VRC^ObGHZ*T8Zv(2)^mK6y z(FjgXkdRA|ydZt#(NYEd|LTnYmAwyFF3?N*XgPuRjLM^fV#N&CbTnJb?k1lGYGUwo L^>bP0l+XkK3w|$q literal 0 HcmV?d00001 diff --git a/src/org/openstreetmap/gui/jmapviewer/images/plus.png b/src/org/openstreetmap/gui/jmapviewer/images/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..6d7df3cc54b7b332f277fb68d03e5080906d3510 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kmUKs7M+SzC{oH>NS%G}c0*}aI z1_mZ85N7;mrWptnlqhkHC<)F_D=AMbN@WO0%*-p%^K%VRC^ObGHZ*T8Zv(1{_jGX# z(FjgXaA56mcqgN{8T2ka?MFwtR->h6hGuzYDb>rT*w^n`b+ zZU5Y-wS9JoJ@LbM(%ShKo}6pUNpL%~#^IZt3h(ZQ95EA9jY1=iM~n<5=iEwd3jclw PTE*b$>gTe~DWM4fE@?position specifies the + * coordinates within g + * + * @param g + * @param position + */ + public void paint(Graphics g, Point position); +} diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java new file mode 100644 index 0000000..e1b4023 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java @@ -0,0 +1,39 @@ +package org.openstreetmap.gui.jmapviewer.interfaces; + +//License: GPL. Copyright 2009 by Stefan Zeller + +import java.awt.Graphics; +import java.awt.Point; + +import org.openstreetmap.gui.jmapviewer.Coordinate; +import org.openstreetmap.gui.jmapviewer.JMapViewer; + +/** + * Interface to be implemented by rectangles that can be displayed on the map. + * + * @author Stefan Zeller + * @see JMapViewer#addMapRectangle(MapRectangle) + * @see JMapViewer#getMapRectangleList() + * @date 21.06.2009S + */ +public interface MapRectangle { + + /** + * @return Latitude/Longitude of top left of rectangle + */ + public Coordinate getTopLeft(); + + /** + * @return Latitude/Longitude of bottom right of rectangle + */ + public Coordinate getBottomRight(); + + /** + * Paints the map rectangle on the map. The topLeft and + * bottomRight are specifying the coordinates within g + * + * @param g + * @param position + */ + public void paint(Graphics g, Point topLeft, Point bottomRight); +} diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java new file mode 100644 index 0000000..7571c81 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java @@ -0,0 +1,45 @@ +package org.openstreetmap.gui.jmapviewer.interfaces; + +import org.openstreetmap.gui.jmapviewer.JMapViewer; +import org.openstreetmap.gui.jmapviewer.Tile; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +/** + * Implement this interface for creating your custom tile cache for + * {@link JMapViewer}. + * + * @author Jan Peter Stotz + */ +public interface TileCache { + + /** + * Retrieves a tile from the cache if present, otherwise null + * will be returned. + * + * @param source + * @param x + * tile number on the x axis of the tile to be retrieved + * @param y + * tile number on the y axis of the tile to be retrieved + * @param z + * zoom level of the tile to be retrieved + * @return the requested tile or null if the tile is not + * present in the cache + */ + public Tile getTile(TileSource source, int x, int y, int z); + + /** + * Adds a tile to the cache. How long after adding a tile can be retrieved + * via {@link #getTile(int, int, int)} is unspecified and depends on the + * implementation. + * + * @param tile + */ + public void addTile(Tile tile); + + /** + * @return the number of tiles hold by the cache + */ + public int getTileCount(); +} diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java new file mode 100644 index 0000000..ce12fdc --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java @@ -0,0 +1,26 @@ +package org.openstreetmap.gui.jmapviewer.interfaces; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +/** + * Interface for implementing a tile loader. Tiles are usually loaded via HTTP + * or from a file. + * + * @author Jan Peter Stotz + */ +public interface TileLoader { + + /** + * A typical {@link #createTileLoaderJob(int, int, int)} implementation + * should create and return a new {@link Job} instance that performs the + * load action. + * + * @param tileLayerSource + * @param tilex + * @param tiley + * @param zoom + * @returns {@link Runnable} implementation that performs the desired load + * action. + */ + public Runnable createTileLoaderJob(TileSource tileLayerSource, int tilex, int tiley, int zoom); +} diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoaderListener.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoaderListener.java new file mode 100644 index 0000000..e68057d --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoaderListener.java @@ -0,0 +1,18 @@ +package org.openstreetmap.gui.jmapviewer.interfaces; + +import org.openstreetmap.gui.jmapviewer.Tile; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +public interface TileLoaderListener { + + /** + * Will be called if a new {@link Tile} has been loaded successfully. + * Loaded can mean downloaded or loaded from file cache. + * + * @param tile + */ + public void tileLoadingFinished(Tile tile, boolean success); + + public TileCache getTileCache(); +} diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java new file mode 100644 index 0000000..1704b62 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java @@ -0,0 +1,134 @@ +package org.openstreetmap.gui.jmapviewer.interfaces; + +import java.awt.Image; +import java.io.IOException; + +import org.openstreetmap.gui.jmapviewer.Coordinate; +import org.openstreetmap.gui.jmapviewer.JMapViewer; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +/** + * + * @author Jan Peter Stotz + */ +public interface TileSource { + + /** + * Specifies the different mechanisms for detecting updated tiles + * respectively only download newer tiles than those stored locally. + * + *
    + *
  • {@link #IfNoneMatch} Server provides ETag header entry for all tiles + * and supports conditional download via If-None-Match + * header entry.
  • + *
  • {@link #ETag} Server provides ETag header entry for all tiles but + * does not support conditional download via + * If-None-Match header entry.
  • + *
  • {@link #IfModifiedSince} Server provides Last-Modified header entry + * for all tiles and supports conditional download via + * If-Modified-Since header entry.
  • + *
  • {@link #LastModified} Server provides Last-Modified header entry for + * all tiles but does not support conditional download via + * If-Modified-Since header entry.
  • + *
  • {@link #None} The server does not support any of the listed + * mechanisms.
  • + *
+ * + */ + public enum TileUpdate { + IfNoneMatch, ETag, IfModifiedSince, LastModified, None + } + + /** + * Specifies the maximum zoom value. The number of zoom levels is [0.. + * {@link #getMaxZoom()}]. + * + * @return maximum zoom value that has to be smaller or equal to + * {@link JMapViewer#MAX_ZOOM} + */ + public int getMaxZoom(); + + /** + * Specifies the minimum zoom value. This value is usually 0. + * Only for maps that cover a certain region up to a limited zoom level + * this method should return a value different than 0. + * + * @return minimum zoom value - usually 0 + */ + public int getMinZoom(); + + /** + * @return The supported tile update mechanism + * @see TileUpdate + */ + public TileUpdate getTileUpdate(); + + /** + * A tile layer name has to be unique and has to consist only of characters + * valid for filenames. + * + * @return Name of the tile layer + */ + public String getName(); + + /** + * Constructs the tile url. + * + * @param zoom + * @param tilex + * @param tiley + * @return fully qualified url for downloading the specified tile image + */ + public String getTileUrl(int zoom, int tilex, int tiley) throws IOException; + + /** + * Specifies the tile image type. For tiles rendered by Mapnik or + * Osmarenderer this is usually "png". + * + * @return file extension of the tile image type + */ + public String getTileType(); + + /** + * Specifies how large each tile is. + * @return The size of a single tile in pixels. + */ + public int getTileSize(); + + /** + * @return True if the tile source requires attribution in text or image form. + */ + public boolean requiresAttribution(); + + /** + * @param zoom The optional zoom level for the view. + * @param botRight The bottom right of the bounding box for attribution. + * @param topLeft The top left of the bounding box for attribution. + * @return Attribution text for the image source. + */ + public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight); + + /** + * @return The URL for the attribution image. Null if no image should be displayed. + */ + public Image getAttributionImage(); + + /** + * @return The URL to open when the user clicks the attribution image. + */ + public String getAttributionLinkURL(); + + /** + * @return The URL to open when the user clicks the attribution "Terms of Use" text. + */ + public String getTermsOfUseURL(); + + public double latToTileY(double lat, int zoom); + + public double lonToTileX(double lon, int zoom); + + public double tileYToLat(int y, int zoom); + + public double tileXToLon(int x, int zoom); +} diff --git a/src/org/openstreetmap/gui/jmapviewer/package.html b/src/org/openstreetmap/gui/jmapviewer/package.html new file mode 100644 index 0000000..18ff699 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/package.html @@ -0,0 +1,14 @@ + + +

org.openstreetmap.gui.jmapviewer

+

This package and all sub-packages are belonging to the Java +component JMapViewer +

+

JMapViewer is designed to run as stand-alone component without +any further requirements. Therefore please do not add any code that +depends on other libraries or applications. Only functions and methods +provided by the runtime library of Java 5 should be used.

+

In particular, this concerns the developer of JOSM!

+

2009-08-10 Jan Peter Stotz jpstotz@gmx.de (maintainer of JMapViewer)

+ + \ No newline at end of file diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java new file mode 100644 index 0000000..a6e495e --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java @@ -0,0 +1,114 @@ +/** + * + */ +package org.openstreetmap.gui.jmapviewer.tilesources; + +import java.awt.Image; +import java.io.IOException; + +import javax.swing.ImageIcon; + +import org.openstreetmap.gui.jmapviewer.Coordinate; +import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; + +public abstract class AbstractOsmTileSource implements TileSource { + + protected String name; + protected String baseUrl; + protected String attrImgUrl; + + public AbstractOsmTileSource(String name, String base_url) { + this(name, base_url, null); + } + + public AbstractOsmTileSource(String name, String base_url, String attr_img_url) { + this.name = name; + this.baseUrl = base_url; + attrImgUrl = attr_img_url; + } + + public String getName() { + return name; + } + + public int getMaxZoom() { + return 18; + } + + public int getMinZoom() { + return 0; + } + + public String getExtension() { + return "png"; + } + + /** + * @throws IOException when subclass cannot return the tile URL + */ + public String getTilePath(int zoom, int tilex, int tiley) throws IOException { + return "/" + zoom + "/" + tilex + "/" + tiley + "." + getExtension(); + } + + public String getBaseUrl() { + return this.baseUrl; + } + + public String getTileUrl(int zoom, int tilex, int tiley) throws IOException { + return this.getBaseUrl() + getTilePath(zoom, tilex, tiley); + } + + @Override + public String toString() { + return getName(); + } + + public String getTileType() { + return "png"; + } + + public int getTileSize() { + return 256; + } + + public Image getAttributionImage() { + if (attrImgUrl != null) + return new ImageIcon(attrImgUrl).getImage(); + else + return null; + } + + public boolean requiresAttribution() { + return true; + } + + public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) { + return "© OpenStreetMap contributors, CC-BY-SA "; + } + + public String getAttributionLinkURL() { + return "http://openstreetmap.org/"; + } + + public String getTermsOfUseURL() { + return "http://www.openstreetmap.org/copyright"; + } + + public double latToTileY(double lat, int zoom) { + double l = lat / 180 * Math.PI; + double pf = Math.log(Math.tan(l) + (1 / Math.cos(l))); + return Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI; + } + + public double lonToTileX(double lon, int zoom) { + return Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0; + } + + public double tileYToLat(int y, int zoom) { + return Math.atan(Math.sinh(Math.PI - (Math.PI * y / Math.pow(2.0, zoom - 1)))) * 180 / Math.PI; + } + + public double tileXToLon(int x, int zoom) { + return x * 45.0 / Math.pow(2.0, zoom - 3) - 180.0; + } +} \ No newline at end of file diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/OfflineOsmTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/OfflineOsmTileSource.java new file mode 100644 index 0000000..ced7b82 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/OfflineOsmTileSource.java @@ -0,0 +1,29 @@ +package org.openstreetmap.gui.jmapviewer.tilesources; + +public class OfflineOsmTileSource extends AbstractOsmTileSource { + + private final int minZoom; + private final int maxZoom; + + public OfflineOsmTileSource(String path, int minZoom, int maxZoom) { + super("Offline from "+path, path); + this.minZoom = minZoom; + this.maxZoom = maxZoom; + } + + @Override + public int getMaxZoom() { + return maxZoom; + } + + @Override + public int getMinZoom() { + return minZoom; + } + + @Override + public TileUpdate getTileUpdate() { + return TileUpdate.None; + } + +} diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java new file mode 100644 index 0000000..d34c74a --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java @@ -0,0 +1,83 @@ +package org.openstreetmap.gui.jmapviewer.tilesources; + +public class OsmTileSource { + + public static final String MAP_MAPNIK = "http://tile.openstreetmap.org"; + public static final String MAP_OSMA = "http://tah.openstreetmap.org/Tiles"; + + public static class Mapnik extends AbstractOsmTileSource { + public Mapnik() { + super("Mapnik", MAP_MAPNIK); + } + + public TileUpdate getTileUpdate() { + return TileUpdate.IfNoneMatch; + } + + } + + public static class CycleMap extends AbstractOsmTileSource { + + private static final String PATTERN = "http://%s.tile.opencyclemap.org/cycle"; + + private static final String[] SERVER = { "a", "b", "c" }; + + private int SERVER_NUM = 0; + + public CycleMap() { + super("OSM Cycle Map", PATTERN); + } + + @Override + public String getBaseUrl() { + String url = String.format(this.baseUrl, new Object[] { SERVER[SERVER_NUM] }); + SERVER_NUM = (SERVER_NUM + 1) % SERVER.length; + return url; + } + + @Override + public int getMaxZoom() { + return 17; + } + + public TileUpdate getTileUpdate() { + return TileUpdate.LastModified; + } + + } + + public static abstract class OsmaSource extends AbstractOsmTileSource { + String osmaSuffix; + + public OsmaSource(String name, String osmaSuffix) { + super(name, MAP_OSMA); + this.osmaSuffix = osmaSuffix; + } + + @Override + public int getMaxZoom() { + return 17; + } + + @Override + public String getBaseUrl() { + return MAP_OSMA + "/" + osmaSuffix; + } + + public TileUpdate getTileUpdate() { + return TileUpdate.IfModifiedSince; + } + } + + public static class TilesAtHome extends OsmaSource { + public TilesAtHome() { + super("TilesAtHome", "tile"); + } + } + + public static class Maplint extends OsmaSource { + public Maplint() { + super("Maplint", "maplint"); + } + } +} diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java new file mode 100644 index 0000000..7d37634 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java @@ -0,0 +1,20 @@ +package org.openstreetmap.gui.jmapviewer.tilesources; + + +public class TMSTileSource extends AbstractOsmTileSource { + private int maxZoom; + + public TMSTileSource(String name, String url, int maxZoom) { + super(name, url); + this.maxZoom = maxZoom; + } + + @Override + public int getMaxZoom() { + return (maxZoom == 0) ? super.getMaxZoom() : maxZoom; + } + + public TileUpdate getTileUpdate() { + return TileUpdate.IfNoneMatch; + } +} diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java new file mode 100644 index 0000000..be05e77 --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java @@ -0,0 +1,28 @@ +package org.openstreetmap.gui.jmapviewer.tilesources; + + +public class TemplatedTMSTileSource extends AbstractOsmTileSource { + private int maxZoom; + + public TemplatedTMSTileSource(String name, String url, int maxZoom) { + super(name, url); + this.maxZoom = maxZoom; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return this.baseUrl + .replaceAll("\\{zoom\\}", Integer.toString(zoom)) + .replaceAll("\\{x\\}", Integer.toString(tilex)) + .replaceAll("\\{y\\}", Integer.toString(tiley)); + + } + + @Override + public int getMaxZoom() { + return (maxZoom == 0) ? super.getMaxZoom() : maxZoom; + } + + public TileUpdate getTileUpdate() { + return TileUpdate.IfNoneMatch; + } +} \ No newline at end of file