commit b7d843338f47b3d4102af1d37f21e2e52214d1b2 Author: antoine.gagneux Date: Tue Feb 21 17:04:04 2023 +0100 Premier Depot 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 0000000..1d90fd3 Binary files /dev/null and b/region-auvergne-rhone-alpes_logo.png differ diff --git a/src/ClassePrincipale.java b/src/ClassePrincipale.java new file mode 100644 index 0000000..8d5d302 --- /dev/null +++ b/src/ClassePrincipale.java @@ -0,0 +1,8 @@ +public class ClassePrincipale { + + public static void main(String[] args) { + new MaJFrameDemo().setVisible(true); + new MaJFrameSimple().setVisible(true); + } + +} diff --git a/src/MaJFrameDemo.java b/src/MaJFrameDemo.java new file mode 100644 index 0000000..b1ee4a0 --- /dev/null +++ b/src/MaJFrameDemo.java @@ -0,0 +1,192 @@ + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import org.openstreetmap.gui.jmapviewer.JMapViewer; +import org.openstreetmap.gui.jmapviewer.MapMarkerDot; +import org.openstreetmap.gui.jmapviewer.MapMarkerImage; +import org.openstreetmap.gui.jmapviewer.MapMarkerLabel; +import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; + +/** + * + * Demonstrates the usage of {@link JMapViewer} + * + * @author Jan Peter Stotz + * + */ +public class MaJFrameDemo extends JFrame { + + private static final long serialVersionUID = 1L; + private final JMapViewer map; + + static { + System.setProperty("http.agent", "Gluon Mobile/1.0.3"); + } + + public MaJFrameDemo() { + super("OSM Demo"); + setSize(400, 400); + map = new JMapViewer(); + getContentPane().setLayout(new BorderLayout()); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setExtendedState(JFrame.MAXIMIZED_BOTH); + JPanel panel = new JPanel(); + JPanel helpPanel = new JPanel(); + getContentPane().add(panel, BorderLayout.NORTH); + getContentPane().add(helpPanel, BorderLayout.SOUTH); + JLabel helpLabel = new JLabel("Utilisez le bouton droit de la souris pour vous déplacer,\n" + +"Double-clic gauche ou molette de la souris pour zoomer."); + helpPanel.add(helpLabel); + panel.setLayout(new GridLayout(0, 1, 0, 0)); + + JPanel panel_1 = new JPanel(); + panel.add(panel_1); + + JCheckBox showMapMarker = new JCheckBox("Marqueurs de carte visibles"); + showMapMarker.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + map.setMapMarkerVisible(showMapMarker.isSelected()); + } + }); + showMapMarker.setSelected(true); + panel_1.add(showMapMarker); + + JCheckBox showTileGrid = new JCheckBox("Grille de tuiles visible"); + showTileGrid.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + map.setTileGridVisible(showTileGrid.isSelected()); + } + }); + showTileGrid.setSelected(false); + panel_1.add(showTileGrid); + + JCheckBox showZoomControls = new JCheckBox("Afficher les contrôles de zoom"); + showZoomControls.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + map.setZoomContolsVisible(showZoomControls.isSelected()); + } + }); + showZoomControls.setSelected(true); + panel_1.add(showZoomControls); + + JPanel panel_2 = new JPanel(); + panel.add(panel_2); + + JButton buttonFit = new JButton("Adapter la carte aux marqueurs"); + buttonFit.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + map.setDisplayToFitMapMarkers(); + } + }); + panel_2.add(buttonFit); + + JButton buttonMarkerPoint = new JButton("Marqueurs points"); + buttonMarkerPoint.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + addMarkerDot(100); + } + }); + panel_2.add(buttonMarkerPoint); + + JButton buttonMarkerImg = new JButton("Marqueurs images"); + buttonMarkerImg.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + addMarkerImg(50); + } + }); + panel_2.add(buttonMarkerImg); + + JButton buttonMarkerLabel = new JButton("Marqueurs labels"); + buttonMarkerLabel.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + addMarkerLabel(50); + } + + + }); + panel_2.add(buttonMarkerLabel); + + JButton buttonCleanMarkers = new JButton("Supprimer tous les marqueurs"); + buttonCleanMarkers.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + cleanMarkers(); + } + }); + panel_2.add(buttonCleanMarkers); + getContentPane().add(map, BorderLayout.CENTER); + + // + addMarkerDot(10); + map.addPropertyChangeListener(getName(), new PropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + // TODO Auto-generated method stub + System.out.println("on property change"); + + } + }); + + // map.setDisplayPositionByLatLon(49.807, 8.6, 11); + // map.setTileGridVisible(true); + } + + private void cleanMarkers() { + List 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 0000000..ae4367e Binary files /dev/null and b/src/org/openstreetmap/gui/jmapviewer/images/bing_maps.png differ 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 0000000..c6a4b4b Binary files /dev/null and b/src/org/openstreetmap/gui/jmapviewer/images/error.png differ diff --git a/src/org/openstreetmap/gui/jmapviewer/images/hourglass.png b/src/org/openstreetmap/gui/jmapviewer/images/hourglass.png new file mode 100644 index 0000000..172ac96 Binary files /dev/null and b/src/org/openstreetmap/gui/jmapviewer/images/hourglass.png differ 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 0000000..4093e64 Binary files /dev/null and b/src/org/openstreetmap/gui/jmapviewer/images/minus.png differ 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 0000000..6d7df3c Binary files /dev/null and b/src/org/openstreetmap/gui/jmapviewer/images/plus.png differ diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/MapMarker.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/MapMarker.java new file mode 100644 index 0000000..4a96a8d --- /dev/null +++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/MapMarker.java @@ -0,0 +1,37 @@ +package org.openstreetmap.gui.jmapviewer.interfaces; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Graphics; +import java.awt.Point; + +import org.openstreetmap.gui.jmapviewer.JMapViewer; + +/** + * Interface to be implemented by all one dimensional elements that can be displayed on the map. + * + * @author Jan Peter Stotz + * @see JMapViewer#addMapMarker(MapMarker) + * @see JMapViewer#getMapMarkerList() + */ +public interface MapMarker { + + /** + * @return Latitude of the map marker position + */ + public double getLat(); + + /** + * @return Longitude of the map marker position + */ + public double getLon(); + + /** + * Paints the map marker on the map. The 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