[jmapviewer] 01/07: Imported Upstream version 1.06+dfsg

Sebastiaan Couwenberg sebastic at moszumanska.debian.org
Thu Mar 5 07:51:34 UTC 2015


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository jmapviewer.

commit 43dc3940d645144d571bfdb3d240f41cb9e10cbd
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Thu Mar 5 08:31:12 2015 +0100

    Imported Upstream version 1.06+dfsg
---
 Readme.txt                                         |    6 +-
 .../gui/jmapviewer/AbstractLayer.java              |  176 +-
 .../openstreetmap/gui/jmapviewer/Coordinate.java   |  158 +-
 .../gui/jmapviewer/DefaultMapController.java       |  358 ++--
 src/org/openstreetmap/gui/jmapviewer/Demo.java     |  528 ++---
 .../gui/jmapviewer/JMapController.java             |   68 +-
 .../openstreetmap/gui/jmapviewer/JMapViewer.java   | 2220 ++++++++++----------
 .../gui/jmapviewer/JobDispatcher.java              |  352 ++--
 .../gui/jmapviewer/MapMarkerCircle.java            |  212 +-
 .../openstreetmap/gui/jmapviewer/MapMarkerDot.java |  104 +-
 .../gui/jmapviewer/MemoryTileCache.java            |  452 ++--
 .../gui/jmapviewer/OsmFileCacheTileLoader.java     | 1007 ++++-----
 .../openstreetmap/gui/jmapviewer/OsmMercator.java  |  348 +--
 .../gui/jmapviewer/OsmTileLoader.java              |  256 +--
 src/org/openstreetmap/gui/jmapviewer/Tile.java     |  668 +++---
 .../gui/jmapviewer/TileController.java             |  174 +-
 .../gui/jmapviewer/interfaces/MapMarker.java       |  108 +-
 .../gui/jmapviewer/interfaces/MapRectangle.java    |   76 +-
 .../gui/jmapviewer/interfaces/TileCache.java       |  100 +-
 .../gui/jmapviewer/interfaces/TileJob.java         |   40 +-
 .../gui/jmapviewer/interfaces/TileLoader.java      |   46 +-
 .../jmapviewer/interfaces/TileLoaderListener.java  |   32 +-
 .../gui/jmapviewer/interfaces/TileSource.java      |  316 +--
 .../tilesources/AbstractTMSTileSource.java         |  250 +--
 .../tilesources/BingAerialTileSource.java          |  610 +++---
 .../gui/jmapviewer/tilesources/OsmTileSource.java  |  156 +-
 .../jmapviewer/tilesources/ScanexTileSource.java   |  316 +--
 .../gui/jmapviewer/tilesources/TMSTileSource.java  |   68 +-
 .../tilesources/TemplatedTMSTileSource.java        |  183 +-
 29 files changed, 4699 insertions(+), 4689 deletions(-)

diff --git a/Readme.txt b/Readme.txt
index ef51e62..4c90acf 100644
--- a/Readme.txt
+++ b/Readme.txt
@@ -2,15 +2,15 @@ JMapViewer
 
 (c) 2007, Tim Haussmann
 (c) 2008-2012, Jan Peter Stotz
-(c) 2009-2013, Dirk Stöcker
+(c) 2009-2015, Dirk Stöcker
 (c) 2009, Stefan Zeller
 (c) 2009, Karl Guggisberg
 (c) 2009, Dave Hansen
 (c) 2010-2011, Ian Dees
 (c) 2010-2011, Michael Vigovsky
-(c) 2011-2013, Paul Hartmann
+(c) 2011-2015, Paul Hartmann
 (c) 2011-2014, Gleb Smirnoff
-(c) 2011-2014, Vincent Privat
+(c) 2011-2015, Vincent Privat
 (c) 2011, Jason Huntley
 (c) 2012, Simon Legner
 (c) 2012, Teemu Koskinen
diff --git a/src/org/openstreetmap/gui/jmapviewer/AbstractLayer.java b/src/org/openstreetmap/gui/jmapviewer/AbstractLayer.java
index e288087..cc79f22 100644
--- a/src/org/openstreetmap/gui/jmapviewer/AbstractLayer.java
+++ b/src/org/openstreetmap/gui/jmapviewer/AbstractLayer.java
@@ -1,88 +1,88 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class AbstractLayer {
-    private LayerGroup parent;
-    private String name;
-    private String description;
-    private Style style;
-    private Boolean visible;
-    private Boolean visibleTexts=true;
-
-    public AbstractLayer(String name){
-        this(name, (String)null);
-    }
-    public AbstractLayer(String name, String description){
-        this(name, description, MapMarkerCircle.getDefaultStyle());
-    }
-    public AbstractLayer(String name, Style style){
-        this(name, null, style);
-    }
-    public AbstractLayer(String name, String description, Style style){
-        this(null, name, description, style);
-    }
-    public AbstractLayer(LayerGroup parent, String name){
-        this(parent, name, MapMarkerCircle.getDefaultStyle());
-    }
-    public AbstractLayer(LayerGroup parent, String name, Style style){
-        this(parent, name, null, style);
-    }
-    public AbstractLayer(LayerGroup parent, String name, String description, Style style){
-        setParent(parent);
-        setName(name);
-        setDescription(description);
-        setStyle(style);
-        setVisible(true);
-
-        if(parent!=null) parent.add(this);
-    }
-    public LayerGroup getParent() {
-        return parent;
-    }
-    public void setParent(LayerGroup parent) {
-        this.parent = parent;
-    }
-    public String getName() {
-        return name;
-    }
-    public void setName(String name) {
-        this.name = name;
-    }
-    public String getDescription() {
-        return description;
-    }
-    public void setDescription(String description) {
-        this.description = description;
-    }
-    public Style getStyle() {
-        return style;
-    }
-    public void setStyle(Style style) {
-        this.style = style;
-    }
-    public Boolean isVisible() {
-        return visible;
-    }
-    public void setVisible(Boolean visible) {
-        this.visible = visible;
-    }
-    public static <E> List<E> add(List<E> list, E element) {
-        if(element!=null){
-            if(list==null) list = new ArrayList<>();
-            if(!list.contains(element)) list.add(element);
-        }
-        return list;
-    }
-    public Boolean isVisibleTexts() {
-        return visibleTexts;
-    }
-    public void setVisibleTexts(Boolean visibleTexts) {
-        this.visibleTexts = visibleTexts;
-    }
-    public String toString(){
-        return name;
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AbstractLayer {
+    private LayerGroup parent;
+    private String name;
+    private String description;
+    private Style style;
+    private Boolean visible;
+    private Boolean visibleTexts=true;
+
+    public AbstractLayer(String name){
+        this(name, (String)null);
+    }
+    public AbstractLayer(String name, String description){
+        this(name, description, MapMarkerCircle.getDefaultStyle());
+    }
+    public AbstractLayer(String name, Style style){
+        this(name, null, style);
+    }
+    public AbstractLayer(String name, String description, Style style){
+        this(null, name, description, style);
+    }
+    public AbstractLayer(LayerGroup parent, String name){
+        this(parent, name, MapMarkerCircle.getDefaultStyle());
+    }
+    public AbstractLayer(LayerGroup parent, String name, Style style){
+        this(parent, name, null, style);
+    }
+    public AbstractLayer(LayerGroup parent, String name, String description, Style style){
+        setParent(parent);
+        setName(name);
+        setDescription(description);
+        setStyle(style);
+        setVisible(true);
+
+        if(parent!=null) parent.add(this);
+    }
+    public LayerGroup getParent() {
+        return parent;
+    }
+    public void setParent(LayerGroup parent) {
+        this.parent = parent;
+    }
+    public String getName() {
+        return name;
+    }
+    public void setName(String name) {
+        this.name = name;
+    }
+    public String getDescription() {
+        return description;
+    }
+    public void setDescription(String description) {
+        this.description = description;
+    }
+    public Style getStyle() {
+        return style;
+    }
+    public void setStyle(Style style) {
+        this.style = style;
+    }
+    public Boolean isVisible() {
+        return visible;
+    }
+    public void setVisible(Boolean visible) {
+        this.visible = visible;
+    }
+    public static <E> List<E> add(List<E> list, E element) {
+        if(element!=null){
+            if(list==null) list = new ArrayList<>();
+            if(!list.contains(element)) list.add(element);
+        }
+        return list;
+    }
+    public Boolean isVisibleTexts() {
+        return visibleTexts;
+    }
+    public void setVisibleTexts(Boolean visibleTexts) {
+        this.visibleTexts = visibleTexts;
+    }
+    public String toString(){
+        return name;
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/Coordinate.java b/src/org/openstreetmap/gui/jmapviewer/Coordinate.java
index 25a482d..a4a9e97 100644
--- a/src/org/openstreetmap/gui/jmapviewer/Coordinate.java
+++ b/src/org/openstreetmap/gui/jmapviewer/Coordinate.java
@@ -1,79 +1,79 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.awt.geom.Point2D;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-import java.util.Objects;
-
-import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
-
-/**
- * This class encapsulates a Point2D.Double and provide access
- * via <tt>lat</tt> and <tt>lon</tt>.
- *
- * @author Jan Peter Stotz
- *
- */
-public class Coordinate implements Serializable, ICoordinate {
-    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 + "]";
-    }
-
-    @Override
-    public int hashCode() {
-        int hash = 3;
-        hash = 61 * hash + Objects.hashCode(this.data);
-        return hash;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Coordinate other = (Coordinate) obj;
-        if (!Objects.equals(this.data, other.data)) {
-            return false;
-        }
-        return true;
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.awt.geom.Point2D;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Objects;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
+
+/**
+ * This class encapsulates a Point2D.Double and provide access
+ * via <tt>lat</tt> and <tt>lon</tt>.
+ *
+ * @author Jan Peter Stotz
+ *
+ */
+public class Coordinate implements Serializable, ICoordinate {
+    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 + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 61 * hash + Objects.hashCode(this.data);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Coordinate other = (Coordinate) obj;
+        if (!Objects.equals(this.data, other.data)) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java b/src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java
index e254c42..20c8c12 100644
--- a/src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java
+++ b/src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java
@@ -1,179 +1,179 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-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 || isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_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;
-        }
-    }
-
-    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:
-     * <ul>
-     * <li>{@link MouseEvent#BUTTON1} (left mouse button)</li>
-     * <li>{@link MouseEvent#BUTTON2} (middle mouse button)</li>
-     * <li>{@link MouseEvent#BUTTON3} (right mouse button)</li>
-     * </ul>
-     *
-     * @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");
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+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 || isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_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;
+        }
+    }
+
+    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:
+     * <ul>
+     * <li>{@link MouseEvent#BUTTON1} (left mouse button)</li>
+     * <li>{@link MouseEvent#BUTTON2} (middle mouse button)</li>
+     * <li>{@link MouseEvent#BUTTON3} (right mouse button)</li>
+     * </ul>
+     *
+     * @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/Demo.java b/src/org/openstreetmap/gui/jmapviewer/Demo.java
index 57ee480..ddbb451 100644
--- a/src/org/openstreetmap/gui/jmapviewer/Demo.java
+++ b/src/org/openstreetmap/gui/jmapviewer/Demo.java
@@ -1,264 +1,264 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.awt.BorderLayout;
-import java.awt.Cursor;
-import java.awt.Point;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.io.IOException;
-
-import javax.swing.JButton;
-import javax.swing.JCheckBox;
-import javax.swing.JComboBox;
-import javax.swing.JFrame;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent;
-import org.openstreetmap.gui.jmapviewer.interfaces.JMapViewerEventListener;
-import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
-import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
-import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
-import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
-import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource;
-import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource;
-import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
-
-/**
- * Demonstrates the usage of {@link JMapViewer}
- *
- * @author Jan Peter Stotz
- *
- */
-public class Demo extends JFrame implements JMapViewerEventListener  {
-
-    private static final long serialVersionUID = 1L;
-
-    private JMapViewerTree treeMap = null;
-
-    private JLabel zoomLabel=null;
-    private JLabel zoomValue=null;
-
-    private JLabel mperpLabelName=null;
-    private JLabel mperpLabelValue = null;
-
-    /**
-     * Constructs the {@code Demo}.
-     */
-    public Demo() {
-        super("JMapViewer Demo");
-        setSize(400, 400);
-
-        treeMap = new JMapViewerTree("Zones");
-
-        // Listen to the map viewer for user operations so components will
-        // receive events and update
-        map().addJMVListener(this);
-
-        setLayout(new BorderLayout());
-        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-        setExtendedState(JFrame.MAXIMIZED_BOTH);
-        JPanel panel = new JPanel();
-        JPanel panelTop = new JPanel();
-        JPanel panelBottom = new JPanel();
-        JPanel helpPanel = new JPanel();
-
-        mperpLabelName=new JLabel("Meters/Pixels: ");
-        mperpLabelValue=new JLabel(String.format("%s",map().getMeterPerPixel()));
-
-        zoomLabel=new JLabel("Zoom: ");
-        zoomValue=new JLabel(String.format("%s", map().getZoom()));
-
-        add(panel, BorderLayout.NORTH);
-        add(helpPanel, BorderLayout.SOUTH);
-        panel.setLayout(new BorderLayout());
-        panel.add(panelTop, BorderLayout.NORTH);
-        panel.add(panelBottom, BorderLayout.SOUTH);
-        JLabel helpLabel = new JLabel("Use right mouse button to move,\n "
-                + "left double click or mouse wheel to zoom.");
-        helpPanel.add(helpLabel);
-        JButton button = new JButton("setDisplayToFitMapMarkers");
-        button.addActionListener(new ActionListener() {
-
-            public void actionPerformed(ActionEvent e) {
-                map().setDisplayToFitMapMarkers();
-            }
-        });
-        JComboBox<TileSource> tileSourceSelector = new JComboBox<>(new TileSource[] {
-                new OsmTileSource.Mapnik(),
-                new OsmTileSource.CycleMap(),
-                new BingAerialTileSource(),
-                new MapQuestOsmTileSource(),
-                new MapQuestOpenAerialTileSource() });
-        tileSourceSelector.addItemListener(new ItemListener() {
-            public void itemStateChanged(ItemEvent e) {
-                map().setTileSource((TileSource) e.getItem());
-            }
-        });
-        JComboBox<TileLoader> tileLoaderSelector;
-        try {
-            tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmFileCacheTileLoader(map()), new OsmTileLoader(map()) });
-        } catch (IOException e) {
-            tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmTileLoader(map()) });
-        }
-        tileLoaderSelector.addItemListener(new ItemListener() {
-            public void itemStateChanged(ItemEvent e) {
-                map().setTileLoader((TileLoader) e.getItem());
-            }
-        });
-        map().setTileLoader((TileLoader) tileLoaderSelector.getSelectedItem());
-        panelTop.add(tileSourceSelector);
-        panelTop.add(tileLoaderSelector);
-        final JCheckBox showMapMarker = new JCheckBox("Map markers visible");
-        showMapMarker.setSelected(map().getMapMarkersVisible());
-        showMapMarker.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-                map().setMapMarkerVisible(showMapMarker.isSelected());
-            }
-        });
-        panelBottom.add(showMapMarker);
-        ///
-        final JCheckBox showTreeLayers = new JCheckBox("Tree Layers visible");
-        showTreeLayers.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-                treeMap.setTreeVisible(showTreeLayers.isSelected());
-            }
-        });
-        panelBottom.add(showTreeLayers);
-        ///
-        final JCheckBox showToolTip = new JCheckBox("ToolTip visible");
-        showToolTip.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-                map().setToolTipText(null);
-            }
-        });
-        panelBottom.add(showToolTip);
-        ///
-        final JCheckBox showTileGrid = new JCheckBox("Tile grid visible");
-        showTileGrid.setSelected(map().isTileGridVisible());
-        showTileGrid.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-                map().setTileGridVisible(showTileGrid.isSelected());
-            }
-        });
-        panelBottom.add(showTileGrid);
-        final JCheckBox showZoomControls = new JCheckBox("Show zoom controls");
-        showZoomControls.setSelected(map().getZoomControlsVisible());
-        showZoomControls.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-                map().setZoomContolsVisible(showZoomControls.isSelected());
-            }
-        });
-        panelBottom.add(showZoomControls);
-        final JCheckBox scrollWrapEnabled = new JCheckBox("Scrollwrap enabled");
-        scrollWrapEnabled.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-                map().setScrollWrapEnabled(scrollWrapEnabled.isSelected());
-            }
-        });
-        panelBottom.add(scrollWrapEnabled);
-        panelBottom.add(button);
-
-        panelTop.add(zoomLabel);
-        panelTop.add(zoomValue);
-        panelTop.add(mperpLabelName);
-        panelTop.add(mperpLabelValue);
-
-        add(treeMap, BorderLayout.CENTER);
-
-        //
-        LayerGroup germanyGroup = new LayerGroup("Germany");
-        Layer germanyWestLayer = germanyGroup.addLayer("Germany West");
-        Layer germanyEastLayer = germanyGroup.addLayer("Germany East");
-        MapMarkerDot eberstadt = new MapMarkerDot(germanyEastLayer, "Eberstadt", 49.814284999, 8.642065999);
-        MapMarkerDot ebersheim = new MapMarkerDot(germanyWestLayer, "Ebersheim", 49.91, 8.24);
-        MapMarkerDot empty = new MapMarkerDot(germanyEastLayer, 49.71, 8.64);
-        MapMarkerDot darmstadt = new MapMarkerDot(germanyEastLayer, "Darmstadt", 49.8588, 8.643);
-        map().addMapMarker(eberstadt);
-        map().addMapMarker(ebersheim);
-        map().addMapMarker(empty);
-        Layer franceLayer = treeMap.addLayer("France");
-        map().addMapMarker(new MapMarkerDot(franceLayer, "La Gallerie", 48.71, -1));
-        map().addMapMarker(new MapMarkerDot(43.604, 1.444));
-        map().addMapMarker(new MapMarkerCircle(53.343, -6.267, 0.666));
-        map().addMapRectangle(new MapRectangleImpl(new Coordinate(53.343, -6.267), new Coordinate(43.604, 1.444)));
-        map().addMapMarker(darmstadt);
-        treeMap.addLayer(germanyWestLayer);
-        treeMap.addLayer(germanyEastLayer);
-
-        MapPolygon bermudas = new MapPolygonImpl(c(49,1), c(45,10), c(40,5));
-        map().addMapPolygon( bermudas );
-        map().addMapPolygon( new MapPolygonImpl(germanyEastLayer, "Riedstadt", ebersheim, darmstadt, eberstadt, empty));
-
-        map().addMapMarker(new MapMarkerCircle(germanyWestLayer, "North of Suisse", new Coordinate(48, 7), .5));
-        Layer spain = treeMap.addLayer("Spain");
-        map().addMapMarker(new MapMarkerCircle(spain, "La Garena", new Coordinate(40.4838, -3.39), .002));
-        spain.setVisible(false);
-
-        Layer wales = treeMap.addLayer("UK");
-        map().addMapRectangle(new MapRectangleImpl(wales, "Wales", c(53.35,-4.57), c(51.64,-2.63)));
-
-        // map.setDisplayPosition(new Coordinate(49.807, 8.6), 11);
-        // map.setTileGridVisible(true);
-
-        map().addMouseListener(new MouseAdapter() {
-            @Override
-            public void mouseClicked(MouseEvent e) {
-                if (e.getButton() == MouseEvent.BUTTON1) {
-                    map().getAttribution().handleAttribution(e.getPoint(), true);
-                }
-            }
-        });
-
-        map().addMouseMotionListener(new MouseAdapter() {
-            @Override
-            public void mouseMoved(MouseEvent e) {
-                Point p = e.getPoint();
-                boolean cursorHand = map().getAttribution().handleAttributionCursor(p);
-                if (cursorHand) {
-                    map().setCursor(new Cursor(Cursor.HAND_CURSOR));
-                } else {
-                    map().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
-                }
-                if(showToolTip.isSelected()) map().setToolTipText(map().getPosition(p).toString());
-            }
-        });
-    }
-    private JMapViewer map(){
-        return treeMap.getViewer();
-    }
-    private static Coordinate c(double lat, double lon){
-        return new Coordinate(lat, lon);
-    }
-
-    /**
-     * @param args
-     */
-    public static void main(String[] args) {
-        // java.util.Properties systemProperties = System.getProperties();
-        // systemProperties.setProperty("http.proxyHost", "localhost");
-        // systemProperties.setProperty("http.proxyPort", "8008");
-        new Demo().setVisible(true);
-    }
-
-    private void updateZoomParameters() {
-        if (mperpLabelValue!=null)
-            mperpLabelValue.setText(String.format("%s",map().getMeterPerPixel()));
-        if (zoomValue!=null)
-            zoomValue.setText(String.format("%s", map().getZoom()));
-    }
-
-    @Override
-    public void processCommand(JMVCommandEvent command) {
-        if (command.getCommand().equals(JMVCommandEvent.COMMAND.ZOOM) ||
-                command.getCommand().equals(JMVCommandEvent.COMMAND.MOVE)) {
-            updateZoomParameters();
-        }
-    }
-
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent;
+import org.openstreetmap.gui.jmapviewer.interfaces.JMapViewerEventListener;
+import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
+
+/**
+ * Demonstrates the usage of {@link JMapViewer}
+ *
+ * @author Jan Peter Stotz
+ *
+ */
+public class Demo extends JFrame implements JMapViewerEventListener  {
+
+    private static final long serialVersionUID = 1L;
+
+    private JMapViewerTree treeMap = null;
+
+    private JLabel zoomLabel=null;
+    private JLabel zoomValue=null;
+
+    private JLabel mperpLabelName=null;
+    private JLabel mperpLabelValue = null;
+
+    /**
+     * Constructs the {@code Demo}.
+     */
+    public Demo() {
+        super("JMapViewer Demo");
+        setSize(400, 400);
+
+        treeMap = new JMapViewerTree("Zones");
+
+        // Listen to the map viewer for user operations so components will
+        // receive events and update
+        map().addJMVListener(this);
+
+        setLayout(new BorderLayout());
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        setExtendedState(JFrame.MAXIMIZED_BOTH);
+        JPanel panel = new JPanel();
+        JPanel panelTop = new JPanel();
+        JPanel panelBottom = new JPanel();
+        JPanel helpPanel = new JPanel();
+
+        mperpLabelName=new JLabel("Meters/Pixels: ");
+        mperpLabelValue=new JLabel(String.format("%s",map().getMeterPerPixel()));
+
+        zoomLabel=new JLabel("Zoom: ");
+        zoomValue=new JLabel(String.format("%s", map().getZoom()));
+
+        add(panel, BorderLayout.NORTH);
+        add(helpPanel, BorderLayout.SOUTH);
+        panel.setLayout(new BorderLayout());
+        panel.add(panelTop, BorderLayout.NORTH);
+        panel.add(panelBottom, BorderLayout.SOUTH);
+        JLabel helpLabel = new JLabel("Use right mouse button to move,\n "
+                + "left double click or mouse wheel to zoom.");
+        helpPanel.add(helpLabel);
+        JButton button = new JButton("setDisplayToFitMapMarkers");
+        button.addActionListener(new ActionListener() {
+
+            public void actionPerformed(ActionEvent e) {
+                map().setDisplayToFitMapMarkers();
+            }
+        });
+        JComboBox<TileSource> tileSourceSelector = new JComboBox<>(new TileSource[] {
+                new OsmTileSource.Mapnik(),
+                new OsmTileSource.CycleMap(),
+                new BingAerialTileSource(),
+                new MapQuestOsmTileSource(),
+                new MapQuestOpenAerialTileSource() });
+        tileSourceSelector.addItemListener(new ItemListener() {
+            public void itemStateChanged(ItemEvent e) {
+                map().setTileSource((TileSource) e.getItem());
+            }
+        });
+        JComboBox<TileLoader> tileLoaderSelector;
+        try {
+            tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmFileCacheTileLoader(map()), new OsmTileLoader(map()) });
+        } catch (IOException e) {
+            tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmTileLoader(map()) });
+        }
+        tileLoaderSelector.addItemListener(new ItemListener() {
+            public void itemStateChanged(ItemEvent e) {
+                map().setTileLoader((TileLoader) e.getItem());
+            }
+        });
+        map().setTileLoader((TileLoader) tileLoaderSelector.getSelectedItem());
+        panelTop.add(tileSourceSelector);
+        panelTop.add(tileLoaderSelector);
+        final JCheckBox showMapMarker = new JCheckBox("Map markers visible");
+        showMapMarker.setSelected(map().getMapMarkersVisible());
+        showMapMarker.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                map().setMapMarkerVisible(showMapMarker.isSelected());
+            }
+        });
+        panelBottom.add(showMapMarker);
+        ///
+        final JCheckBox showTreeLayers = new JCheckBox("Tree Layers visible");
+        showTreeLayers.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                treeMap.setTreeVisible(showTreeLayers.isSelected());
+            }
+        });
+        panelBottom.add(showTreeLayers);
+        ///
+        final JCheckBox showToolTip = new JCheckBox("ToolTip visible");
+        showToolTip.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                map().setToolTipText(null);
+            }
+        });
+        panelBottom.add(showToolTip);
+        ///
+        final JCheckBox showTileGrid = new JCheckBox("Tile grid visible");
+        showTileGrid.setSelected(map().isTileGridVisible());
+        showTileGrid.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                map().setTileGridVisible(showTileGrid.isSelected());
+            }
+        });
+        panelBottom.add(showTileGrid);
+        final JCheckBox showZoomControls = new JCheckBox("Show zoom controls");
+        showZoomControls.setSelected(map().getZoomControlsVisible());
+        showZoomControls.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                map().setZoomContolsVisible(showZoomControls.isSelected());
+            }
+        });
+        panelBottom.add(showZoomControls);
+        final JCheckBox scrollWrapEnabled = new JCheckBox("Scrollwrap enabled");
+        scrollWrapEnabled.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                map().setScrollWrapEnabled(scrollWrapEnabled.isSelected());
+            }
+        });
+        panelBottom.add(scrollWrapEnabled);
+        panelBottom.add(button);
+
+        panelTop.add(zoomLabel);
+        panelTop.add(zoomValue);
+        panelTop.add(mperpLabelName);
+        panelTop.add(mperpLabelValue);
+
+        add(treeMap, BorderLayout.CENTER);
+
+        //
+        LayerGroup germanyGroup = new LayerGroup("Germany");
+        Layer germanyWestLayer = germanyGroup.addLayer("Germany West");
+        Layer germanyEastLayer = germanyGroup.addLayer("Germany East");
+        MapMarkerDot eberstadt = new MapMarkerDot(germanyEastLayer, "Eberstadt", 49.814284999, 8.642065999);
+        MapMarkerDot ebersheim = new MapMarkerDot(germanyWestLayer, "Ebersheim", 49.91, 8.24);
+        MapMarkerDot empty = new MapMarkerDot(germanyEastLayer, 49.71, 8.64);
+        MapMarkerDot darmstadt = new MapMarkerDot(germanyEastLayer, "Darmstadt", 49.8588, 8.643);
+        map().addMapMarker(eberstadt);
+        map().addMapMarker(ebersheim);
+        map().addMapMarker(empty);
+        Layer franceLayer = treeMap.addLayer("France");
+        map().addMapMarker(new MapMarkerDot(franceLayer, "La Gallerie", 48.71, -1));
+        map().addMapMarker(new MapMarkerDot(43.604, 1.444));
+        map().addMapMarker(new MapMarkerCircle(53.343, -6.267, 0.666));
+        map().addMapRectangle(new MapRectangleImpl(new Coordinate(53.343, -6.267), new Coordinate(43.604, 1.444)));
+        map().addMapMarker(darmstadt);
+        treeMap.addLayer(germanyWestLayer);
+        treeMap.addLayer(germanyEastLayer);
+
+        MapPolygon bermudas = new MapPolygonImpl(c(49,1), c(45,10), c(40,5));
+        map().addMapPolygon( bermudas );
+        map().addMapPolygon( new MapPolygonImpl(germanyEastLayer, "Riedstadt", ebersheim, darmstadt, eberstadt, empty));
+
+        map().addMapMarker(new MapMarkerCircle(germanyWestLayer, "North of Suisse", new Coordinate(48, 7), .5));
+        Layer spain = treeMap.addLayer("Spain");
+        map().addMapMarker(new MapMarkerCircle(spain, "La Garena", new Coordinate(40.4838, -3.39), .002));
+        spain.setVisible(false);
+
+        Layer wales = treeMap.addLayer("UK");
+        map().addMapRectangle(new MapRectangleImpl(wales, "Wales", c(53.35,-4.57), c(51.64,-2.63)));
+
+        // map.setDisplayPosition(new Coordinate(49.807, 8.6), 11);
+        // map.setTileGridVisible(true);
+
+        map().addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                if (e.getButton() == MouseEvent.BUTTON1) {
+                    map().getAttribution().handleAttribution(e.getPoint(), true);
+                }
+            }
+        });
+
+        map().addMouseMotionListener(new MouseAdapter() {
+            @Override
+            public void mouseMoved(MouseEvent e) {
+                Point p = e.getPoint();
+                boolean cursorHand = map().getAttribution().handleAttributionCursor(p);
+                if (cursorHand) {
+                    map().setCursor(new Cursor(Cursor.HAND_CURSOR));
+                } else {
+                    map().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
+                }
+                if(showToolTip.isSelected()) map().setToolTipText(map().getPosition(p).toString());
+            }
+        });
+    }
+    private JMapViewer map(){
+        return treeMap.getViewer();
+    }
+    private static Coordinate c(double lat, double lon){
+        return new Coordinate(lat, lon);
+    }
+
+    /**
+     * @param args
+     */
+    public static void main(String[] args) {
+        // java.util.Properties systemProperties = System.getProperties();
+        // systemProperties.setProperty("http.proxyHost", "localhost");
+        // systemProperties.setProperty("http.proxyPort", "8008");
+        new Demo().setVisible(true);
+    }
+
+    private void updateZoomParameters() {
+        if (mperpLabelValue!=null)
+            mperpLabelValue.setText(String.format("%s",map().getMeterPerPixel()));
+        if (zoomValue!=null)
+            zoomValue.setText(String.format("%s", map().getZoom()));
+    }
+
+    @Override
+    public void processCommand(JMVCommandEvent command) {
+        if (command.getCommand().equals(JMVCommandEvent.COMMAND.ZOOM) ||
+                command.getCommand().equals(JMVCommandEvent.COMMAND.MOVE)) {
+            updateZoomParameters();
+        }
+    }
+
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/JMapController.java b/src/org/openstreetmap/gui/jmapviewer/JMapController.java
index 8de5300..0417563 100644
--- a/src/org/openstreetmap/gui/jmapviewer/JMapController.java
+++ b/src/org/openstreetmap/gui/jmapviewer/JMapController.java
@@ -1,34 +1,34 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-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:
- * <ul>
- * <li>{@link MouseListener}</li>
- * <li>{@link MouseMotionListener}</li>
- * <li>{@link MouseWheelListener}</li>
- * </ul>
- *
- * @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);
-    }
-
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+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:
+ * <ul>
+ * <li>{@link MouseListener}</li>
+ * <li>{@link MouseMotionListener}</li>
+ * <li>{@link MouseWheelListener}</li>
+ * </ul>
+ *
+ * @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
index 6ccbe2e..d0d1237 100644
--- a/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
+++ b/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
@@ -1,1110 +1,1110 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.awt.Dimension;
-import java.awt.Font;
-import java.awt.Graphics;
-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.util.Collections;
-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 javax.swing.event.EventListenerList;
-
-import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent;
-import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent.COMMAND;
-import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
-import org.openstreetmap.gui.jmapviewer.interfaces.JMapViewerEventListener;
-import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
-import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
-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 {
-
-    public static boolean debug = false;
-
-    /**
-     * 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<MapMarker> mapMarkerList;
-    protected List<MapRectangle> mapRectangleList;
-    protected List<MapPolygon> mapPolygonList;
-
-    protected boolean mapMarkersVisible;
-    protected boolean mapRectanglesVisible;
-    protected boolean mapPolygonsVisible;
-
-    protected boolean tileGridVisible;
-    protected boolean scrollWrapEnabled;
-
-    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;
-
-    public static enum ZOOM_BUTTON_STYLE {
-        HORIZONTAL,
-        VERTICAL
-    }
-
-    protected ZOOM_BUTTON_STYLE zoomButtonStyle;
-
-    protected TileSource tileSource;
-
-    protected AttributionSupport attribution = new AttributionSupport();
-
-    /**
-     * Creates a standard {@link JMapViewer} instance that can be controlled via
-     * mouse: hold right mouse button for moving, double click left mouse button
-     * or use mouse wheel for zooming. Loaded tiles are stored in a
-     * {@link MemoryTileCache} and the tile loader uses 4 parallel threads for
-     * retrieving the tiles.
-     */
-    public JMapViewer() {
-        this(new MemoryTileCache(), 8);
-        new DefaultMapController(this);
-    }
-
-    /**
-     * Creates a new {@link JMapViewer} instance.
-     * @param tileCache The cache where to store tiles
-     * @param downloadThreadCount The number of parallel threads for retrieving the tiles
-     */
-    public JMapViewer(TileCache tileCache, int downloadThreadCount) {
-        JobDispatcher.setMaxWorkers(downloadThreadCount);
-        tileSource = new OsmTileSource.Mapnik();
-        tileController = new TileController(tileSource, tileCache, this);
-        mapMarkerList = Collections.synchronizedList(new LinkedList<MapMarker>());
-        mapPolygonList = Collections.synchronizedList(new LinkedList<MapPolygon>());
-        mapRectangleList = Collections.synchronizedList(new LinkedList<MapRectangle>());
-        mapMarkersVisible = true;
-        mapRectanglesVisible = true;
-        mapPolygonsVisible = true;
-        tileGridVisible = false;
-        setLayout(null);
-        initializeZoomSlider();
-        setMinimumSize(new Dimension(tileSource.getTileSize(), tileSource.getTileSize()));
-        setPreferredSize(new Dimension(400, 400));
-        setDisplayPosition(new Coordinate(50, 9), 3);
-    }
-
-    @Override
-    public String getToolTipText(MouseEvent event) {
-        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());
-            }
-        });
-        zoomSlider.setFocusable(false);
-        add(zoomSlider);
-        int size = 18;
-        try {
-            ImageIcon icon = new ImageIcon(JMapViewer.class.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();
-            }
-        });
-        zoomInButton.setFocusable(false);
-        add(zoomInButton);
-        try {
-            ImageIcon icon = new ImageIcon(JMapViewer.class.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();
-            }
-        });
-        zoomOutButton.setFocusable(false);
-        add(zoomOutButton);
-    }
-
-    /**
-     * Changes the map pane so that it is centered on the specified coordinate
-     * at the given zoom level.
-     *
-     * @param to
-     *            specified coordinate
-     * @param zoom
-     *            {@link #MIN_ZOOM} <= zoom level <= {@link #MAX_ZOOM}
-     */
-    public void setDisplayPosition(Coordinate to, int zoom) {
-        setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), to, zoom);
-    }
-
-    /**
-     * Changes the map pane so that the specified coordinate at the given zoom
-     * level is displayed on the map at the screen coordinate
-     * <code>mapPoint</code>.
-     *
-     * @param mapPoint
-     *            point on the map denoted in pixels where the coordinate should
-     *            be set
-     * @param to
-     *            specified coordinate
-     * @param zoom
-     *            {@link #MIN_ZOOM} <= zoom level <=
-     *            {@link TileSource#getMaxZoom()}
-     */
-    public void setDisplayPosition(Point mapPoint, Coordinate to, int zoom) {
-        int x = tileSource.LonToX(to.getLon(), zoom);
-        int y = tileSource.LatToY(to.getLat(), 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 chosen map elements are visible.
-     */
-    public void setDisplayToFitMapElements(boolean markers, boolean rectangles, boolean polygons) {
-        int nbElemToCheck = 0;
-        if (markers && mapMarkerList != null)
-            nbElemToCheck += mapMarkerList.size();
-        if (rectangles && mapRectangleList != null)
-            nbElemToCheck += mapRectangleList.size();
-        if (polygons && mapPolygonList != null)
-            nbElemToCheck += mapPolygonList.size();
-        if (nbElemToCheck == 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();
-
-        if (markers) {
-            synchronized (mapMarkerList) {
-                for (MapMarker marker : mapMarkerList) {
-                    if (marker.isVisible()) {
-                        int x = tileSource.LonToX(marker.getLon(), mapZoomMax);
-                        int y = tileSource.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);
-                    }
-                }
-            }
-        }
-
-        if (rectangles) {
-            synchronized (mapRectangleList) {
-                for (MapRectangle rectangle : mapRectangleList) {
-                    if (rectangle.isVisible()) {
-                        x_max = Math.max(x_max, tileSource.LonToX(rectangle.getBottomRight().getLon(), mapZoomMax));
-                        y_max = Math.max(y_max, tileSource.LatToY(rectangle.getTopLeft().getLat(), mapZoomMax));
-                        x_min = Math.min(x_min, tileSource.LonToX(rectangle.getTopLeft().getLon(), mapZoomMax));
-                        y_min = Math.min(y_min, tileSource.LatToY(rectangle.getBottomRight().getLat(), mapZoomMax));
-                    }
-                }
-            }
-        }
-
-        if (polygons) {
-            synchronized (mapPolygonList) {
-                for (MapPolygon polygon : mapPolygonList) {
-                    if (polygon.isVisible()) {
-                        for (ICoordinate c : polygon.getPoints()) {
-                            int x = tileSource.LonToX(c.getLon(), mapZoomMax);
-                            int y = tileSource.LatToY(c.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());
-        int newZoom = mapZoomMax;
-        int x = x_max - x_min;
-        int y = y_max - y_min;
-        while (x > width || y > height) {
-            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 setDisplayToFitMapMarkers() {
-        setDisplayToFitMapElements(true, false, false);
-    }
-
-    /**
-     * Sets the displayed map pane and zoom level so that all map rectangles are visible.
-     */
-    public void setDisplayToFitMapRectangles() {
-        setDisplayToFitMapElements(false, true, false);
-    }
-
-    /**
-     * Sets the displayed map pane and zoom level so that all map polygons are visible.
-     */
-    public void setDisplayToFitMapPolygons() {
-        setDisplayToFitMapElements(false, false, true);
-    }
-
-    /**
-     * @return the center
-     */
-    public Point getCenter() {
-        return center;
-    }
-
-    /**
-     * @param center the center to set
-     */
-    public void setCenter(Point center) {
-        this.center = center;
-    }
-
-    /**
-     * Calculates the latitude/longitude coordinate of the center of the
-     * currently displayed map area.
-     *
-     * @return latitude / longitude
-     */
-    public Coordinate getPosition() {
-        double lon = tileSource.XToLon(center.x, zoom);
-        double lat = tileSource.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 latitude / longitude
-     */
-    public Coordinate getPosition(int mapPointX, int mapPointY) {
-        int x = center.x + mapPointX - getWidth() / 2;
-        int y = center.y + mapPointY - getHeight() / 2;
-        double lon = tileSource.XToLon(x, zoom);
-        double lat = tileSource.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 <code>null</code> if the point is not visible
-     *         and checkOutside set to <code>true</code>
-     */
-    public Point getMapPosition(double lat, double lon, boolean checkOutside) {
-        int x = tileSource.LonToX(lon, zoom);
-        int y = tileSource.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 Latitude
-     * @param offset Offset respect Latitude
-     * @param checkOutside
-     * @return Integer the radius in pixels
-     */
-    public Integer getLatOffset(double lat, double offset, boolean checkOutside) {
-        int y = tileSource.LatToY(lat + offset, zoom);
-        y -= center.y - getHeight() / 2;
-        if (checkOutside) {
-            if (y < 0 || y > getHeight())
-                return null;
-        }
-        return y;
-    }
-
-    /**
-     * Calculates the position on the map of a given coordinate
-     *
-     * @param lat
-     * @param lon
-     * @return point on the map or <code>null</code> if the point is not visible
-     */
-    public Point getMapPosition(double lat, double lon) {
-        return getMapPosition(lat, lon, true);
-    }
-
-    /**
-     * Calculates the position on the map of a given coordinate
-     *
-     * @param marker MapMarker object that define the x,y coordinate
-     * @return Integer the radius in pixels
-     */
-    public Integer getRadius(MapMarker marker, Point p) {
-        if (marker.getMarkerStyle() == MapMarker.STYLE.FIXED)
-            return (int) marker.getRadius();
-        else if (p != null) {
-            Integer radius = getLatOffset(marker.getLat(), marker.getRadius(), false);
-            radius = radius == null ? null : p.y - radius.intValue();
-            return radius;
-        } else
-            return null;
-    }
-
-    /**
-     * Calculates the position on the map of a given coordinate
-     *
-     * @param coord
-     * @return point on the map or <code>null</code> 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 <code>null</code> if the point is not visible
-     *         and checkOutside set to <code>true</code>
-     */
-    public Point getMapPosition(ICoordinate coord, boolean checkOutside) {
-        if (coord != null)
-            return getMapPosition(coord.getLat(), coord.getLon(), checkOutside);
-        else
-            return null;
-    }
-
-    /**
-     * Gets the meter per pixel.
-     *
-     * @return the meter per pixel
-     * @author Jason Huntley
-     */
-    public double getMeterPerPixel() {
-        Point origin = new Point(5, 5);
-        Point center = new Point(getWidth() / 2, getHeight() / 2);
-
-        double pDistance = center.distance(origin);
-
-        Coordinate originCoord = getPosition(origin);
-        Coordinate centerCoord = getPosition(center);
-
-        double mDistance = tileSource.getDistance(originCoord.getLat(), originCoord.getLon(),
-                centerCoord.getLat(), centerCoord.getLon());
-
-        return mDistance / pDistance;
-    }
-
-    @Override
-    protected void paintComponent(Graphics g) {
-        super.paintComponent(g);
-
-        int iMove = 0;
-
-        int tilesize = tileSource.getTileSize();
-        int tilex = center.x / tilesize;
-        int tiley = center.y / tilesize;
-        int 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();
-
-        // calculate the length of the grid (number of squares per edge)
-        int gridLength = 1 << zoom;
-
-        // paint the tiles in a spiral, starting from center of the map
-        boolean painted = true;
-        int x = 0;
-        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;
-                        if (scrollWrapEnabled) {
-                            // in case tilex is out of bounds, grab the tile to use for wrapping
-                            int tilexWrap = (((tilex % gridLength) + gridLength) % gridLength);
-                            tile = tileController.getTile(tilexWrap, tiley, zoom);
-                        } else {
-                            tile = tileController.getTile(tilex, tiley, zoom);
-                        }
-                        if (tile != null) {
-                            tile.paint(g, posx, posy);
-                            if (tileGridVisible) {
-                                g.drawRect(posx, posy, tilesize, tilesize);
-                            }
-                        }
-                        painted = true;
-                    }
-                    Point p = move[iMove];
-                    posx += p.x * tilesize;
-                    posy += p.y * tilesize;
-                    tilex += p.x;
-                    tiley += p.y;
-                }
-                iMove = (iMove + 1) % move.length;
-            }
-        }
-        // outer border of the map
-        int mapSize = tilesize << zoom;
-        if (scrollWrapEnabled) {
-            g.drawLine(0, h2 - center.y, getWidth(), h2 - center.y);
-            g.drawLine(0, h2 - center.y + mapSize, getWidth(), h2 - center.y + mapSize);
-        } else {
-            g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize);
-        }
-
-        // g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20);
-
-        // keep x-coordinates from growing without bound if scroll-wrap is enabled
-        if (scrollWrapEnabled) {
-            center.x = center.x % mapSize;
-        }
-
-        if (mapPolygonsVisible && mapPolygonList != null) {
-            synchronized (mapPolygonList) {
-                for (MapPolygon polygon : mapPolygonList) {
-                    if (polygon.isVisible())
-                        paintPolygon(g, polygon);
-                }
-            }
-        }
-
-        if (mapRectanglesVisible && mapRectangleList != null) {
-            synchronized (mapRectangleList) {
-                for (MapRectangle rectangle : mapRectangleList) {
-                    if (rectangle.isVisible())
-                        paintRectangle(g, rectangle);
-                }
-            }
-        }
-
-        if (mapMarkersVisible && mapMarkerList != null) {
-            synchronized (mapMarkerList) {
-                for (MapMarker marker : mapMarkerList) {
-                    if (marker.isVisible())
-                        paintMarker(g, marker);
-                }
-            }
-        }
-
-        attribution.paintAttribution(g, getWidth(), getHeight(), getPosition(0, 0), getPosition(getWidth(), getHeight()), zoom, this);
-    }
-
-    /**
-     * Paint a single marker.
-     */
-    protected void paintMarker(Graphics g, MapMarker marker) {
-        Point p = getMapPosition(marker.getLat(), marker.getLon(), marker.getMarkerStyle() == MapMarker.STYLE.FIXED);
-        Integer radius = getRadius(marker, p);
-        if (scrollWrapEnabled) {
-            int tilesize = tileSource.getTileSize();
-            int mapSize = tilesize << zoom;
-            if (p == null) {
-                p = getMapPosition(marker.getLat(), marker.getLon(), false);
-                radius = getRadius(marker, p);
-            }
-            marker.paint(g, p, radius);
-            int xSave = p.x;
-            int xWrap = xSave;
-            // overscan of 15 allows up to 30-pixel markers to gracefully scroll off the edge of the panel
-            while ((xWrap -= mapSize) >= -15) {
-                p.x = xWrap;
-                marker.paint(g, p, radius);
-            }
-            xWrap = xSave;
-            while ((xWrap += mapSize) <= getWidth() + 15) {
-                p.x = xWrap;
-                marker.paint(g, p, radius);
-            }
-        } else {
-            if (p != null) {
-                marker.paint(g, p, radius);
-            }
-        }
-    }
-
-    /**
-     * Paint a single rectangle.
-     */
-    protected void paintRectangle(Graphics g, MapRectangle rectangle) {
-        Coordinate topLeft = rectangle.getTopLeft();
-        Coordinate bottomRight = rectangle.getBottomRight();
-        if (topLeft != null && bottomRight != null) {
-            Point pTopLeft = getMapPosition(topLeft, false);
-            Point pBottomRight = getMapPosition(bottomRight, false);
-            if (pTopLeft != null && pBottomRight != null) {
-                rectangle.paint(g, pTopLeft, pBottomRight);
-                if (scrollWrapEnabled) {
-                    int tilesize = tileSource.getTileSize();
-                    int mapSize = tilesize << zoom;
-                    int xTopLeftSave = pTopLeft.x;
-                    int xTopLeftWrap = xTopLeftSave;
-                    int xBottomRightSave = pBottomRight.x;
-                    int xBottomRightWrap = xBottomRightSave;
-                    while ((xBottomRightWrap -= mapSize) >= 0) {
-                        xTopLeftWrap -= mapSize;
-                        pTopLeft.x = xTopLeftWrap;
-                        pBottomRight.x = xBottomRightWrap;
-                        rectangle.paint(g, pTopLeft, pBottomRight);
-                    }
-                    xTopLeftWrap = xTopLeftSave;
-                    xBottomRightWrap = xBottomRightSave;
-                    while ((xTopLeftWrap += mapSize) <= getWidth()) {
-                        xBottomRightWrap += mapSize;
-                        pTopLeft.x = xTopLeftWrap;
-                        pBottomRight.x = xBottomRightWrap;
-                        rectangle.paint(g, pTopLeft, pBottomRight);
-                    }
-
-                }
-            }
-        }
-    }
-
-    /**
-     * Paint a single polygon.
-     */
-    protected void paintPolygon(Graphics g, MapPolygon polygon) {
-        List<? extends ICoordinate> coords = polygon.getPoints();
-        if (coords != null && coords.size() >= 3) {
-            List<Point> points = new LinkedList<>();
-            for (ICoordinate c : coords) {
-                Point p = getMapPosition(c, false);
-                if (p == null) {
-                    return;
-                }
-                points.add(p);
-            }
-            polygon.paint(g, points);
-            if (scrollWrapEnabled) {
-                int tilesize = tileSource.getTileSize();
-                int mapSize = tilesize << zoom;
-                List<Point> pointsWrapped = new LinkedList<>(points);
-                boolean keepWrapping = true;
-                while (keepWrapping) {
-                    for (Point p : pointsWrapped) {
-                        p.x -= mapSize;
-                        if (p.x < 0) {
-                            keepWrapping = false;
-                        }
-                    }
-                    polygon.paint(g, pointsWrapped);
-                }
-                pointsWrapped = new LinkedList<>(points);
-                keepWrapping = true;
-                while (keepWrapping) {
-                    for (Point p : pointsWrapped) {
-                        p.x += mapSize;
-                        if (p.x > getWidth()) {
-                            keepWrapping = false;
-                        }
-                    }
-                    polygon.paint(g, pointsWrapped);
-                }
-            }
-        }
-    }
-
-    /**
-     * Moves the visible map pane.
-     *
-     * @param x
-     *            horizontal movement in pixel.
-     * @param y
-     *            vertical movement in pixel
-     */
-    public void moveMap(int x, int y) {
-        tileController.cancelOutstandingJobs(); // Clear outstanding load
-        center.x += x;
-        center.y += y;
-        repaint();
-        this.fireJMVEvent(new JMVCommandEvent(COMMAND.MOVE, this));
-    }
-
-    /**
-     * @return the current zoom level
-     */
-    public int getZoom() {
-        return zoom;
-    }
-
-    /**
-     * Increases the current zoom level by one
-     */
-    public void zoomIn() {
-        setZoom(zoom + 1);
-    }
-
-    /**
-     * Increases the current zoom level by one
-     * @param mapPoint point to choose as center for new zoom level
-     */
-    public void zoomIn(Point mapPoint) {
-        setZoom(zoom + 1, mapPoint);
-    }
-
-    /**
-     * Decreases the current zoom level by one
-     */
-    public void zoomOut() {
-        setZoom(zoom - 1);
-    }
-
-    /**
-     * Decreases the current zoom level by one
-     *
-     * @param mapPoint point to choose as center for new zoom level
-     */
-    public void zoomOut(Point mapPoint) {
-        setZoom(zoom - 1, mapPoint);
-    }
-
-    /**
-     * Set the zoom level and center point for display
-     *
-     * @param zoom new zoom level
-     * @param mapPoint point to choose as center for new zoom level
-     */
-    public void setZoom(int zoom, Point mapPoint) {
-        if (zoom > tileController.getTileSource().getMaxZoom() || zoom < tileController.getTileSource().getMinZoom()
-                || zoom == this.zoom)
-            return;
-        Coordinate zoomPos = getPosition(mapPoint);
-        tileController.cancelOutstandingJobs(); // Clearing outstanding load
-        // requests
-        setDisplayPosition(mapPoint, zoomPos, zoom);
-
-        this.fireJMVEvent(new JMVCommandEvent(COMMAND.ZOOM, this));
-    }
-
-    /**
-     * Set the zoom level
-     *
-     * @param zoom new zoom level
-     */
-    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<MapMarker> mapMarkerList) {
-        this.mapMarkerList = mapMarkerList;
-        repaint();
-    }
-
-    public List<MapMarker> getMapMarkerList() {
-        return mapMarkerList;
-    }
-
-    public void setMapRectangleList(List<MapRectangle> mapRectangleList) {
-        this.mapRectangleList = mapRectangleList;
-        repaint();
-    }
-
-    public List<MapRectangle> getMapRectangleList() {
-        return mapRectangleList;
-    }
-
-    public void setMapPolygonList(List<MapPolygon> mapPolygonList) {
-        this.mapPolygonList = mapPolygonList;
-        repaint();
-    }
-
-    public List<MapPolygon> getMapPolygonList() {
-        return mapPolygonList;
-    }
-
-    public void addMapMarker(MapMarker marker) {
-        mapMarkerList.add(marker);
-        repaint();
-    }
-
-    public void removeMapMarker(MapMarker marker) {
-        mapMarkerList.remove(marker);
-        repaint();
-    }
-
-    public void removeAllMapMarkers() {
-        mapMarkerList.clear();
-        repaint();
-    }
-
-    public void addMapRectangle(MapRectangle rectangle) {
-        mapRectangleList.add(rectangle);
-        repaint();
-    }
-
-    public void removeMapRectangle(MapRectangle rectangle) {
-        mapRectangleList.remove(rectangle);
-        repaint();
-    }
-
-    public void removeAllMapRectangles() {
-        mapRectangleList.clear();
-        repaint();
-    }
-
-    public void addMapPolygon(MapPolygon polygon) {
-        mapPolygonList.add(polygon);
-        repaint();
-    }
-
-    public void removeMapPolygon(MapPolygon polygon) {
-        mapPolygonList.remove(polygon);
-        repaint();
-    }
-
-    public void removeAllMapPolygons() {
-        mapPolygonList.clear();
-        repaint();
-    }
-
-    public void setZoomContolsVisible(boolean visible) {
-        zoomSlider.setVisible(visible);
-        zoomInButton.setVisible(visible);
-        zoomOutButton.setVisible(visible);
-    }
-
-    public boolean getZoomControlsVisible() {
-        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("Minimum zoom level too low");
-        Coordinate position = getPosition();
-        this.tileSource = tileSource;
-        tileController.setTileSource(tileSource);
-        zoomSlider.setMinimum(tileSource.getMinZoom());
-        zoomSlider.setMaximum(tileSource.getMaxZoom());
-        tileController.cancelOutstandingJobs();
-        if (zoom > tileSource.getMaxZoom()) {
-            setZoom(tileSource.getMaxZoom());
-        }
-        attribution.initialize(tileSource);
-        setDisplayPosition(position, zoom);
-        repaint();
-    }
-
-    public void tileLoadingFinished(Tile tile, boolean success) {
-        repaint();
-    }
-
-    public boolean isMapRectanglesVisible() {
-        return mapRectanglesVisible;
-    }
-
-    /**
-     * Enables or disables painting of the {@link MapRectangle}
-     *
-     * @param mapRectanglesVisible
-     * @see #addMapRectangle(MapRectangle)
-     * @see #getMapRectangleList()
-     */
-    public void setMapRectanglesVisible(boolean mapRectanglesVisible) {
-        this.mapRectanglesVisible = mapRectanglesVisible;
-        repaint();
-    }
-
-    public boolean isMapPolygonsVisible() {
-        return mapPolygonsVisible;
-    }
-
-    /**
-     * Enables or disables painting of the {@link MapPolygon}
-     *
-     * @param mapPolygonsVisible
-     * @see #addMapPolygon(MapPolygon)
-     * @see #getMapPolygonList()
-     */
-    public void setMapPolygonsVisible(boolean mapPolygonsVisible) {
-        this.mapPolygonsVisible = mapPolygonsVisible;
-        repaint();
-    }
-
-    public boolean isScrollWrapEnabled() {
-        return scrollWrapEnabled;
-    }
-
-    public void setScrollWrapEnabled(boolean scrollWrapEnabled) {
-        this.scrollWrapEnabled = scrollWrapEnabled;
-        repaint();
-    }
-
-    public ZOOM_BUTTON_STYLE getZoomButtonStyle() {
-        return zoomButtonStyle;
-    }
-
-    public void setZoomButtonStyle(ZOOM_BUTTON_STYLE style) {
-        zoomButtonStyle = style;
-        if (zoomSlider == null || zoomInButton == null || zoomOutButton == null) {
-            return;
-        }
-        switch (style) {
-        case HORIZONTAL:
-            zoomSlider.setBounds(10, 10, 30, 150);
-            zoomInButton.setBounds(4, 155, 18, 18);
-            zoomOutButton.setBounds(26, 155, 18, 18);
-            break;
-        case VERTICAL:
-            zoomSlider.setBounds(10, 27, 30, 150);
-            zoomInButton.setBounds(14, 8, 20, 20);
-            zoomOutButton.setBounds(14, 176, 20, 20);
-            break;
-        default:
-            zoomSlider.setBounds(10, 10, 30, 150);
-            zoomInButton.setBounds(4, 155, 18, 18);
-            zoomOutButton.setBounds(26, 155, 18, 18);
-            break;
-        }
-        repaint();
-    }
-
-    public TileController getTileController() {
-        return tileController;
-    }
-
-    /**
-     * Return tile information caching class
-     * @see TileLoaderListener#getTileCache()
-     */
-    public TileCache getTileCache() {
-        return tileController.getTileCache();
-    }
-
-    public void setTileLoader(TileLoader loader) {
-        tileController.setTileLoader(loader);
-    }
-
-    public AttributionSupport getAttribution() {
-        return attribution;
-    }
-
-    protected EventListenerList evtListenerList = new EventListenerList();
-
-    /**
-     * @param listener listener to set
-     */
-    public void addJMVListener(JMapViewerEventListener listener) {
-        evtListenerList.add(JMapViewerEventListener.class, listener);
-    }
-
-    /**
-     * @param listener listener to remove
-     */
-    public void removeJMVListener(JMapViewerEventListener listener) {
-        evtListenerList.remove(JMapViewerEventListener.class, listener);
-    }
-
-    /**
-     * Send an update to all objects registered with viewer
-     *
-     * @param evt event to dispatch
-     */
-    void fireJMVEvent(JMVCommandEvent evt) {
-        Object[] listeners = evtListenerList.getListenerList();
-        for (int i = 0; i < listeners.length; i += 2) {
-            if (listeners[i] == JMapViewerEventListener.class) {
-                ((JMapViewerEventListener) listeners[i + 1]).processCommand(evt);
-            }
-        }
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+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.util.Collections;
+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 javax.swing.event.EventListenerList;
+
+import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent;
+import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent.COMMAND;
+import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
+import org.openstreetmap.gui.jmapviewer.interfaces.JMapViewerEventListener;
+import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
+import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
+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 {
+
+    public static boolean debug = false;
+
+    /**
+     * 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<MapMarker> mapMarkerList;
+    protected List<MapRectangle> mapRectangleList;
+    protected List<MapPolygon> mapPolygonList;
+
+    protected boolean mapMarkersVisible;
+    protected boolean mapRectanglesVisible;
+    protected boolean mapPolygonsVisible;
+
+    protected boolean tileGridVisible;
+    protected boolean scrollWrapEnabled;
+
+    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;
+
+    public static enum ZOOM_BUTTON_STYLE {
+        HORIZONTAL,
+        VERTICAL
+    }
+
+    protected ZOOM_BUTTON_STYLE zoomButtonStyle;
+
+    protected TileSource tileSource;
+
+    protected AttributionSupport attribution = new AttributionSupport();
+
+    /**
+     * Creates a standard {@link JMapViewer} instance that can be controlled via
+     * mouse: hold right mouse button for moving, double click left mouse button
+     * or use mouse wheel for zooming. Loaded tiles are stored in a
+     * {@link MemoryTileCache} and the tile loader uses 4 parallel threads for
+     * retrieving the tiles.
+     */
+    public JMapViewer() {
+        this(new MemoryTileCache(), 8);
+        new DefaultMapController(this);
+    }
+
+    /**
+     * Creates a new {@link JMapViewer} instance.
+     * @param tileCache The cache where to store tiles
+     * @param downloadThreadCount The number of parallel threads for retrieving the tiles
+     */
+    public JMapViewer(TileCache tileCache, int downloadThreadCount) {
+        JobDispatcher.setMaxWorkers(downloadThreadCount);
+        tileSource = new OsmTileSource.Mapnik();
+        tileController = new TileController(tileSource, tileCache, this);
+        mapMarkerList = Collections.synchronizedList(new LinkedList<MapMarker>());
+        mapPolygonList = Collections.synchronizedList(new LinkedList<MapPolygon>());
+        mapRectangleList = Collections.synchronizedList(new LinkedList<MapRectangle>());
+        mapMarkersVisible = true;
+        mapRectanglesVisible = true;
+        mapPolygonsVisible = true;
+        tileGridVisible = false;
+        setLayout(null);
+        initializeZoomSlider();
+        setMinimumSize(new Dimension(tileSource.getTileSize(), tileSource.getTileSize()));
+        setPreferredSize(new Dimension(400, 400));
+        setDisplayPosition(new Coordinate(50, 9), 3);
+    }
+
+    @Override
+    public String getToolTipText(MouseEvent event) {
+        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());
+            }
+        });
+        zoomSlider.setFocusable(false);
+        add(zoomSlider);
+        int size = 18;
+        try {
+            ImageIcon icon = new ImageIcon(JMapViewer.class.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();
+            }
+        });
+        zoomInButton.setFocusable(false);
+        add(zoomInButton);
+        try {
+            ImageIcon icon = new ImageIcon(JMapViewer.class.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();
+            }
+        });
+        zoomOutButton.setFocusable(false);
+        add(zoomOutButton);
+    }
+
+    /**
+     * Changes the map pane so that it is centered on the specified coordinate
+     * at the given zoom level.
+     *
+     * @param to
+     *            specified coordinate
+     * @param zoom
+     *            {@link #MIN_ZOOM} <= zoom level <= {@link #MAX_ZOOM}
+     */
+    public void setDisplayPosition(Coordinate to, int zoom) {
+        setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), to, zoom);
+    }
+
+    /**
+     * Changes the map pane so that the specified coordinate at the given zoom
+     * level is displayed on the map at the screen coordinate
+     * <code>mapPoint</code>.
+     *
+     * @param mapPoint
+     *            point on the map denoted in pixels where the coordinate should
+     *            be set
+     * @param to
+     *            specified coordinate
+     * @param zoom
+     *            {@link #MIN_ZOOM} <= zoom level <=
+     *            {@link TileSource#getMaxZoom()}
+     */
+    public void setDisplayPosition(Point mapPoint, Coordinate to, int zoom) {
+        int x = tileSource.LonToX(to.getLon(), zoom);
+        int y = tileSource.LatToY(to.getLat(), 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 chosen map elements are visible.
+     */
+    public void setDisplayToFitMapElements(boolean markers, boolean rectangles, boolean polygons) {
+        int nbElemToCheck = 0;
+        if (markers && mapMarkerList != null)
+            nbElemToCheck += mapMarkerList.size();
+        if (rectangles && mapRectangleList != null)
+            nbElemToCheck += mapRectangleList.size();
+        if (polygons && mapPolygonList != null)
+            nbElemToCheck += mapPolygonList.size();
+        if (nbElemToCheck == 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();
+
+        if (markers) {
+            synchronized (mapMarkerList) {
+                for (MapMarker marker : mapMarkerList) {
+                    if (marker.isVisible()) {
+                        int x = tileSource.LonToX(marker.getLon(), mapZoomMax);
+                        int y = tileSource.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);
+                    }
+                }
+            }
+        }
+
+        if (rectangles) {
+            synchronized (mapRectangleList) {
+                for (MapRectangle rectangle : mapRectangleList) {
+                    if (rectangle.isVisible()) {
+                        x_max = Math.max(x_max, tileSource.LonToX(rectangle.getBottomRight().getLon(), mapZoomMax));
+                        y_max = Math.max(y_max, tileSource.LatToY(rectangle.getTopLeft().getLat(), mapZoomMax));
+                        x_min = Math.min(x_min, tileSource.LonToX(rectangle.getTopLeft().getLon(), mapZoomMax));
+                        y_min = Math.min(y_min, tileSource.LatToY(rectangle.getBottomRight().getLat(), mapZoomMax));
+                    }
+                }
+            }
+        }
+
+        if (polygons) {
+            synchronized (mapPolygonList) {
+                for (MapPolygon polygon : mapPolygonList) {
+                    if (polygon.isVisible()) {
+                        for (ICoordinate c : polygon.getPoints()) {
+                            int x = tileSource.LonToX(c.getLon(), mapZoomMax);
+                            int y = tileSource.LatToY(c.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());
+        int newZoom = mapZoomMax;
+        int x = x_max - x_min;
+        int y = y_max - y_min;
+        while (x > width || y > height) {
+            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 setDisplayToFitMapMarkers() {
+        setDisplayToFitMapElements(true, false, false);
+    }
+
+    /**
+     * Sets the displayed map pane and zoom level so that all map rectangles are visible.
+     */
+    public void setDisplayToFitMapRectangles() {
+        setDisplayToFitMapElements(false, true, false);
+    }
+
+    /**
+     * Sets the displayed map pane and zoom level so that all map polygons are visible.
+     */
+    public void setDisplayToFitMapPolygons() {
+        setDisplayToFitMapElements(false, false, true);
+    }
+
+    /**
+     * @return the center
+     */
+    public Point getCenter() {
+        return center;
+    }
+
+    /**
+     * @param center the center to set
+     */
+    public void setCenter(Point center) {
+        this.center = center;
+    }
+
+    /**
+     * Calculates the latitude/longitude coordinate of the center of the
+     * currently displayed map area.
+     *
+     * @return latitude / longitude
+     */
+    public Coordinate getPosition() {
+        double lon = tileSource.XToLon(center.x, zoom);
+        double lat = tileSource.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 latitude / longitude
+     */
+    public Coordinate getPosition(int mapPointX, int mapPointY) {
+        int x = center.x + mapPointX - getWidth() / 2;
+        int y = center.y + mapPointY - getHeight() / 2;
+        double lon = tileSource.XToLon(x, zoom);
+        double lat = tileSource.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 <code>null</code> if the point is not visible
+     *         and checkOutside set to <code>true</code>
+     */
+    public Point getMapPosition(double lat, double lon, boolean checkOutside) {
+        int x = tileSource.LonToX(lon, zoom);
+        int y = tileSource.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 Latitude
+     * @param offset Offset respect Latitude
+     * @param checkOutside
+     * @return Integer the radius in pixels
+     */
+    public Integer getLatOffset(double lat, double offset, boolean checkOutside) {
+        int y = tileSource.LatToY(lat + offset, zoom);
+        y -= center.y - getHeight() / 2;
+        if (checkOutside) {
+            if (y < 0 || y > getHeight())
+                return null;
+        }
+        return y;
+    }
+
+    /**
+     * Calculates the position on the map of a given coordinate
+     *
+     * @param lat
+     * @param lon
+     * @return point on the map or <code>null</code> if the point is not visible
+     */
+    public Point getMapPosition(double lat, double lon) {
+        return getMapPosition(lat, lon, true);
+    }
+
+    /**
+     * Calculates the position on the map of a given coordinate
+     *
+     * @param marker MapMarker object that define the x,y coordinate
+     * @return Integer the radius in pixels
+     */
+    public Integer getRadius(MapMarker marker, Point p) {
+        if (marker.getMarkerStyle() == MapMarker.STYLE.FIXED)
+            return (int) marker.getRadius();
+        else if (p != null) {
+            Integer radius = getLatOffset(marker.getLat(), marker.getRadius(), false);
+            radius = radius == null ? null : p.y - radius.intValue();
+            return radius;
+        } else
+            return null;
+    }
+
+    /**
+     * Calculates the position on the map of a given coordinate
+     *
+     * @param coord
+     * @return point on the map or <code>null</code> 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 <code>null</code> if the point is not visible
+     *         and checkOutside set to <code>true</code>
+     */
+    public Point getMapPosition(ICoordinate coord, boolean checkOutside) {
+        if (coord != null)
+            return getMapPosition(coord.getLat(), coord.getLon(), checkOutside);
+        else
+            return null;
+    }
+
+    /**
+     * Gets the meter per pixel.
+     *
+     * @return the meter per pixel
+     * @author Jason Huntley
+     */
+    public double getMeterPerPixel() {
+        Point origin = new Point(5, 5);
+        Point center = new Point(getWidth() / 2, getHeight() / 2);
+
+        double pDistance = center.distance(origin);
+
+        Coordinate originCoord = getPosition(origin);
+        Coordinate centerCoord = getPosition(center);
+
+        double mDistance = tileSource.getDistance(originCoord.getLat(), originCoord.getLon(),
+                centerCoord.getLat(), centerCoord.getLon());
+
+        return mDistance / pDistance;
+    }
+
+    @Override
+    protected void paintComponent(Graphics g) {
+        super.paintComponent(g);
+
+        int iMove = 0;
+
+        int tilesize = tileSource.getTileSize();
+        int tilex = center.x / tilesize;
+        int tiley = center.y / tilesize;
+        int 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();
+
+        // calculate the length of the grid (number of squares per edge)
+        int gridLength = 1 << zoom;
+
+        // paint the tiles in a spiral, starting from center of the map
+        boolean painted = true;
+        int x = 0;
+        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;
+                        if (scrollWrapEnabled) {
+                            // in case tilex is out of bounds, grab the tile to use for wrapping
+                            int tilexWrap = (((tilex % gridLength) + gridLength) % gridLength);
+                            tile = tileController.getTile(tilexWrap, tiley, zoom);
+                        } else {
+                            tile = tileController.getTile(tilex, tiley, zoom);
+                        }
+                        if (tile != null) {
+                            tile.paint(g, posx, posy);
+                            if (tileGridVisible) {
+                                g.drawRect(posx, posy, tilesize, tilesize);
+                            }
+                        }
+                        painted = true;
+                    }
+                    Point p = move[iMove];
+                    posx += p.x * tilesize;
+                    posy += p.y * tilesize;
+                    tilex += p.x;
+                    tiley += p.y;
+                }
+                iMove = (iMove + 1) % move.length;
+            }
+        }
+        // outer border of the map
+        int mapSize = tilesize << zoom;
+        if (scrollWrapEnabled) {
+            g.drawLine(0, h2 - center.y, getWidth(), h2 - center.y);
+            g.drawLine(0, h2 - center.y + mapSize, getWidth(), h2 - center.y + mapSize);
+        } else {
+            g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize);
+        }
+
+        // g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20);
+
+        // keep x-coordinates from growing without bound if scroll-wrap is enabled
+        if (scrollWrapEnabled) {
+            center.x = center.x % mapSize;
+        }
+
+        if (mapPolygonsVisible && mapPolygonList != null) {
+            synchronized (mapPolygonList) {
+                for (MapPolygon polygon : mapPolygonList) {
+                    if (polygon.isVisible())
+                        paintPolygon(g, polygon);
+                }
+            }
+        }
+
+        if (mapRectanglesVisible && mapRectangleList != null) {
+            synchronized (mapRectangleList) {
+                for (MapRectangle rectangle : mapRectangleList) {
+                    if (rectangle.isVisible())
+                        paintRectangle(g, rectangle);
+                }
+            }
+        }
+
+        if (mapMarkersVisible && mapMarkerList != null) {
+            synchronized (mapMarkerList) {
+                for (MapMarker marker : mapMarkerList) {
+                    if (marker.isVisible())
+                        paintMarker(g, marker);
+                }
+            }
+        }
+
+        attribution.paintAttribution(g, getWidth(), getHeight(), getPosition(0, 0), getPosition(getWidth(), getHeight()), zoom, this);
+    }
+
+    /**
+     * Paint a single marker.
+     */
+    protected void paintMarker(Graphics g, MapMarker marker) {
+        Point p = getMapPosition(marker.getLat(), marker.getLon(), marker.getMarkerStyle() == MapMarker.STYLE.FIXED);
+        Integer radius = getRadius(marker, p);
+        if (scrollWrapEnabled) {
+            int tilesize = tileSource.getTileSize();
+            int mapSize = tilesize << zoom;
+            if (p == null) {
+                p = getMapPosition(marker.getLat(), marker.getLon(), false);
+                radius = getRadius(marker, p);
+            }
+            marker.paint(g, p, radius);
+            int xSave = p.x;
+            int xWrap = xSave;
+            // overscan of 15 allows up to 30-pixel markers to gracefully scroll off the edge of the panel
+            while ((xWrap -= mapSize) >= -15) {
+                p.x = xWrap;
+                marker.paint(g, p, radius);
+            }
+            xWrap = xSave;
+            while ((xWrap += mapSize) <= getWidth() + 15) {
+                p.x = xWrap;
+                marker.paint(g, p, radius);
+            }
+        } else {
+            if (p != null) {
+                marker.paint(g, p, radius);
+            }
+        }
+    }
+
+    /**
+     * Paint a single rectangle.
+     */
+    protected void paintRectangle(Graphics g, MapRectangle rectangle) {
+        Coordinate topLeft = rectangle.getTopLeft();
+        Coordinate bottomRight = rectangle.getBottomRight();
+        if (topLeft != null && bottomRight != null) {
+            Point pTopLeft = getMapPosition(topLeft, false);
+            Point pBottomRight = getMapPosition(bottomRight, false);
+            if (pTopLeft != null && pBottomRight != null) {
+                rectangle.paint(g, pTopLeft, pBottomRight);
+                if (scrollWrapEnabled) {
+                    int tilesize = tileSource.getTileSize();
+                    int mapSize = tilesize << zoom;
+                    int xTopLeftSave = pTopLeft.x;
+                    int xTopLeftWrap = xTopLeftSave;
+                    int xBottomRightSave = pBottomRight.x;
+                    int xBottomRightWrap = xBottomRightSave;
+                    while ((xBottomRightWrap -= mapSize) >= 0) {
+                        xTopLeftWrap -= mapSize;
+                        pTopLeft.x = xTopLeftWrap;
+                        pBottomRight.x = xBottomRightWrap;
+                        rectangle.paint(g, pTopLeft, pBottomRight);
+                    }
+                    xTopLeftWrap = xTopLeftSave;
+                    xBottomRightWrap = xBottomRightSave;
+                    while ((xTopLeftWrap += mapSize) <= getWidth()) {
+                        xBottomRightWrap += mapSize;
+                        pTopLeft.x = xTopLeftWrap;
+                        pBottomRight.x = xBottomRightWrap;
+                        rectangle.paint(g, pTopLeft, pBottomRight);
+                    }
+
+                }
+            }
+        }
+    }
+
+    /**
+     * Paint a single polygon.
+     */
+    protected void paintPolygon(Graphics g, MapPolygon polygon) {
+        List<? extends ICoordinate> coords = polygon.getPoints();
+        if (coords != null && coords.size() >= 3) {
+            List<Point> points = new LinkedList<>();
+            for (ICoordinate c : coords) {
+                Point p = getMapPosition(c, false);
+                if (p == null) {
+                    return;
+                }
+                points.add(p);
+            }
+            polygon.paint(g, points);
+            if (scrollWrapEnabled) {
+                int tilesize = tileSource.getTileSize();
+                int mapSize = tilesize << zoom;
+                List<Point> pointsWrapped = new LinkedList<>(points);
+                boolean keepWrapping = true;
+                while (keepWrapping) {
+                    for (Point p : pointsWrapped) {
+                        p.x -= mapSize;
+                        if (p.x < 0) {
+                            keepWrapping = false;
+                        }
+                    }
+                    polygon.paint(g, pointsWrapped);
+                }
+                pointsWrapped = new LinkedList<>(points);
+                keepWrapping = true;
+                while (keepWrapping) {
+                    for (Point p : pointsWrapped) {
+                        p.x += mapSize;
+                        if (p.x > getWidth()) {
+                            keepWrapping = false;
+                        }
+                    }
+                    polygon.paint(g, pointsWrapped);
+                }
+            }
+        }
+    }
+
+    /**
+     * Moves the visible map pane.
+     *
+     * @param x
+     *            horizontal movement in pixel.
+     * @param y
+     *            vertical movement in pixel
+     */
+    public void moveMap(int x, int y) {
+        tileController.cancelOutstandingJobs(); // Clear outstanding load
+        center.x += x;
+        center.y += y;
+        repaint();
+        this.fireJMVEvent(new JMVCommandEvent(COMMAND.MOVE, this));
+    }
+
+    /**
+     * @return the current zoom level
+     */
+    public int getZoom() {
+        return zoom;
+    }
+
+    /**
+     * Increases the current zoom level by one
+     */
+    public void zoomIn() {
+        setZoom(zoom + 1);
+    }
+
+    /**
+     * Increases the current zoom level by one
+     * @param mapPoint point to choose as center for new zoom level
+     */
+    public void zoomIn(Point mapPoint) {
+        setZoom(zoom + 1, mapPoint);
+    }
+
+    /**
+     * Decreases the current zoom level by one
+     */
+    public void zoomOut() {
+        setZoom(zoom - 1);
+    }
+
+    /**
+     * Decreases the current zoom level by one
+     *
+     * @param mapPoint point to choose as center for new zoom level
+     */
+    public void zoomOut(Point mapPoint) {
+        setZoom(zoom - 1, mapPoint);
+    }
+
+    /**
+     * Set the zoom level and center point for display
+     *
+     * @param zoom new zoom level
+     * @param mapPoint point to choose as center for new zoom level
+     */
+    public void setZoom(int zoom, Point mapPoint) {
+        if (zoom > tileController.getTileSource().getMaxZoom() || zoom < tileController.getTileSource().getMinZoom()
+                || zoom == this.zoom)
+            return;
+        Coordinate zoomPos = getPosition(mapPoint);
+        tileController.cancelOutstandingJobs(); // Clearing outstanding load
+        // requests
+        setDisplayPosition(mapPoint, zoomPos, zoom);
+
+        this.fireJMVEvent(new JMVCommandEvent(COMMAND.ZOOM, this));
+    }
+
+    /**
+     * Set the zoom level
+     *
+     * @param zoom new zoom level
+     */
+    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<MapMarker> mapMarkerList) {
+        this.mapMarkerList = mapMarkerList;
+        repaint();
+    }
+
+    public List<MapMarker> getMapMarkerList() {
+        return mapMarkerList;
+    }
+
+    public void setMapRectangleList(List<MapRectangle> mapRectangleList) {
+        this.mapRectangleList = mapRectangleList;
+        repaint();
+    }
+
+    public List<MapRectangle> getMapRectangleList() {
+        return mapRectangleList;
+    }
+
+    public void setMapPolygonList(List<MapPolygon> mapPolygonList) {
+        this.mapPolygonList = mapPolygonList;
+        repaint();
+    }
+
+    public List<MapPolygon> getMapPolygonList() {
+        return mapPolygonList;
+    }
+
+    public void addMapMarker(MapMarker marker) {
+        mapMarkerList.add(marker);
+        repaint();
+    }
+
+    public void removeMapMarker(MapMarker marker) {
+        mapMarkerList.remove(marker);
+        repaint();
+    }
+
+    public void removeAllMapMarkers() {
+        mapMarkerList.clear();
+        repaint();
+    }
+
+    public void addMapRectangle(MapRectangle rectangle) {
+        mapRectangleList.add(rectangle);
+        repaint();
+    }
+
+    public void removeMapRectangle(MapRectangle rectangle) {
+        mapRectangleList.remove(rectangle);
+        repaint();
+    }
+
+    public void removeAllMapRectangles() {
+        mapRectangleList.clear();
+        repaint();
+    }
+
+    public void addMapPolygon(MapPolygon polygon) {
+        mapPolygonList.add(polygon);
+        repaint();
+    }
+
+    public void removeMapPolygon(MapPolygon polygon) {
+        mapPolygonList.remove(polygon);
+        repaint();
+    }
+
+    public void removeAllMapPolygons() {
+        mapPolygonList.clear();
+        repaint();
+    }
+
+    public void setZoomContolsVisible(boolean visible) {
+        zoomSlider.setVisible(visible);
+        zoomInButton.setVisible(visible);
+        zoomOutButton.setVisible(visible);
+    }
+
+    public boolean getZoomControlsVisible() {
+        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("Minimum zoom level too low");
+        Coordinate position = getPosition();
+        this.tileSource = tileSource;
+        tileController.setTileSource(tileSource);
+        zoomSlider.setMinimum(tileSource.getMinZoom());
+        zoomSlider.setMaximum(tileSource.getMaxZoom());
+        tileController.cancelOutstandingJobs();
+        if (zoom > tileSource.getMaxZoom()) {
+            setZoom(tileSource.getMaxZoom());
+        }
+        attribution.initialize(tileSource);
+        setDisplayPosition(position, zoom);
+        repaint();
+    }
+
+    public void tileLoadingFinished(Tile tile, boolean success) {
+        repaint();
+    }
+
+    public boolean isMapRectanglesVisible() {
+        return mapRectanglesVisible;
+    }
+
+    /**
+     * Enables or disables painting of the {@link MapRectangle}
+     *
+     * @param mapRectanglesVisible
+     * @see #addMapRectangle(MapRectangle)
+     * @see #getMapRectangleList()
+     */
+    public void setMapRectanglesVisible(boolean mapRectanglesVisible) {
+        this.mapRectanglesVisible = mapRectanglesVisible;
+        repaint();
+    }
+
+    public boolean isMapPolygonsVisible() {
+        return mapPolygonsVisible;
+    }
+
+    /**
+     * Enables or disables painting of the {@link MapPolygon}
+     *
+     * @param mapPolygonsVisible
+     * @see #addMapPolygon(MapPolygon)
+     * @see #getMapPolygonList()
+     */
+    public void setMapPolygonsVisible(boolean mapPolygonsVisible) {
+        this.mapPolygonsVisible = mapPolygonsVisible;
+        repaint();
+    }
+
+    public boolean isScrollWrapEnabled() {
+        return scrollWrapEnabled;
+    }
+
+    public void setScrollWrapEnabled(boolean scrollWrapEnabled) {
+        this.scrollWrapEnabled = scrollWrapEnabled;
+        repaint();
+    }
+
+    public ZOOM_BUTTON_STYLE getZoomButtonStyle() {
+        return zoomButtonStyle;
+    }
+
+    public void setZoomButtonStyle(ZOOM_BUTTON_STYLE style) {
+        zoomButtonStyle = style;
+        if (zoomSlider == null || zoomInButton == null || zoomOutButton == null) {
+            return;
+        }
+        switch (style) {
+        case HORIZONTAL:
+            zoomSlider.setBounds(10, 10, 30, 150);
+            zoomInButton.setBounds(4, 155, 18, 18);
+            zoomOutButton.setBounds(26, 155, 18, 18);
+            break;
+        case VERTICAL:
+            zoomSlider.setBounds(10, 27, 30, 150);
+            zoomInButton.setBounds(14, 8, 20, 20);
+            zoomOutButton.setBounds(14, 176, 20, 20);
+            break;
+        default:
+            zoomSlider.setBounds(10, 10, 30, 150);
+            zoomInButton.setBounds(4, 155, 18, 18);
+            zoomOutButton.setBounds(26, 155, 18, 18);
+            break;
+        }
+        repaint();
+    }
+
+    public TileController getTileController() {
+        return tileController;
+    }
+
+    /**
+     * Return tile information caching class
+     * @see TileController#getTileCache()
+     */
+    public TileCache getTileCache() {
+        return tileController.getTileCache();
+    }
+
+    public void setTileLoader(TileLoader loader) {
+        tileController.setTileLoader(loader);
+    }
+
+    public AttributionSupport getAttribution() {
+        return attribution;
+    }
+
+    protected EventListenerList evtListenerList = new EventListenerList();
+
+    /**
+     * @param listener listener to set
+     */
+    public void addJMVListener(JMapViewerEventListener listener) {
+        evtListenerList.add(JMapViewerEventListener.class, listener);
+    }
+
+    /**
+     * @param listener listener to remove
+     */
+    public void removeJMVListener(JMapViewerEventListener listener) {
+        evtListenerList.remove(JMapViewerEventListener.class, listener);
+    }
+
+    /**
+     * Send an update to all objects registered with viewer
+     *
+     * @param evt event to dispatch
+     */
+    void fireJMVEvent(JMVCommandEvent evt) {
+        Object[] listeners = evtListenerList.getListenerList();
+        for (int i = 0; i < listeners.length; i += 2) {
+            if (listeners[i] == JMapViewerEventListener.class) {
+                ((JMapViewerEventListener) listeners[i + 1]).processCommand(evt);
+            }
+        }
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/JobDispatcher.java b/src/org/openstreetmap/gui/jmapviewer/JobDispatcher.java
index dfd6103..ea22fbc 100644
--- a/src/org/openstreetmap/gui/jmapviewer/JobDispatcher.java
+++ b/src/org/openstreetmap/gui/jmapviewer/JobDispatcher.java
@@ -1,176 +1,176 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.util.concurrent.BlockingDeque;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.TimeUnit;
-
-import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
-
-/**
- * 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 #workerThreadMaxCount} (default: 8). If an instance is idle
- * more than {@link #workerThreadTimeout} 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 BlockingDeque<TileJob> jobQueue = new LinkedBlockingDeque<>();
-
-    protected static int workerThreadMaxCount = 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.
-     */
-    protected static int workerThreadTimeout = 30;
-
-    /**
-     * Type of queue, FIFO if <code>false</code>, LIFO if <code>true</code>
-     */
-    protected boolean modeLIFO = false;
-
-    /**
-     * 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();
-    }
-
-    /**
-     * Function to set the maximum number of workers for tile loading.
-     */
-    static public void setMaxWorkers(int workers) {
-        workerThreadMaxCount = workers;
-    }
-
-    /**
-     * Function to set the LIFO/FIFO mode for tile loading job.
-     *
-     * @param lifo <code>true</code> for LIFO mode, <code>false</code> for FIFO mode
-     */
-    public void setLIFO(boolean lifo) {
-        modeLIFO = lifo;
-    }
-
-    /**
-     * Adds a job to the queue.
-     * Jobs for tiles already contained in the are ignored (using a <code>null</code> tile
-     * prevents skipping).
-     *
-     * @param job the the job to be added
-     */
-    public void addJob(TileJob job) {
-        try {
-            if(job.getTile() != null) {
-                for(TileJob oldJob : jobQueue) {
-                    if(oldJob.getTile() == job.getTile()) {
-                        return;
-                    }
-                }
-            }
-            jobQueue.put(job);
-            if (workerThreadIdleCount == 0 && workerThreadCount < workerThreadMaxCount)
-                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(modeLIFO) {
-                        if (firstThread)
-                            job = jobQueue.takeLast();
-                        else
-                            job = jobQueue.pollLast(workerThreadTimeout, TimeUnit.SECONDS);
-                    } else {
-                        if (firstThread)
-                            job = jobQueue.take();
-                        else
-                            job = jobQueue.poll(workerThreadTimeout, TimeUnit.SECONDS);
-                    }
-                } catch (InterruptedException e1) {
-                    return;
-                } finally {
-                    synchronized (instance) {
-                        workerThreadIdleCount--;
-                    }
-                }
-                if (job == null)
-                    return;
-                try {
-                    job.run();
-                    job = null;
-                } catch (Exception e) {
-                    e.printStackTrace();
-                }
-            }
-        }
-    }
-
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
+
+/**
+ * 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 #workerThreadMaxCount} (default: 8). If an instance is idle
+ * more than {@link #workerThreadTimeout} 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 BlockingDeque<TileJob> jobQueue = new LinkedBlockingDeque<>();
+
+    protected static int workerThreadMaxCount = 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.
+     */
+    protected static int workerThreadTimeout = 30;
+
+    /**
+     * Type of queue, FIFO if <code>false</code>, LIFO if <code>true</code>
+     */
+    protected boolean modeLIFO = false;
+
+    /**
+     * 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();
+    }
+
+    /**
+     * Function to set the maximum number of workers for tile loading.
+     */
+    static public void setMaxWorkers(int workers) {
+        workerThreadMaxCount = workers;
+    }
+
+    /**
+     * Function to set the LIFO/FIFO mode for tile loading job.
+     *
+     * @param lifo <code>true</code> for LIFO mode, <code>false</code> for FIFO mode
+     */
+    public void setLIFO(boolean lifo) {
+        modeLIFO = lifo;
+    }
+
+    /**
+     * Adds a job to the queue.
+     * Jobs for tiles already contained in the are ignored (using a <code>null</code> tile
+     * prevents skipping).
+     *
+     * @param job the the job to be added
+     */
+    public void addJob(TileJob job) {
+        try {
+            if(job.getTile() != null) {
+                for(TileJob oldJob : jobQueue) {
+                    if(oldJob.getTile() == job.getTile()) {
+                        return;
+                    }
+                }
+            }
+            jobQueue.put(job);
+            if (workerThreadIdleCount == 0 && workerThreadCount < workerThreadMaxCount)
+                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(modeLIFO) {
+                        if (firstThread)
+                            job = jobQueue.takeLast();
+                        else
+                            job = jobQueue.pollLast(workerThreadTimeout, TimeUnit.SECONDS);
+                    } else {
+                        if (firstThread)
+                            job = jobQueue.take();
+                        else
+                            job = jobQueue.poll(workerThreadTimeout, 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/MapMarkerCircle.java b/src/org/openstreetmap/gui/jmapviewer/MapMarkerCircle.java
index fb2276b..bfdf7be 100644
--- a/src/org/openstreetmap/gui/jmapviewer/MapMarkerCircle.java
+++ b/src/org/openstreetmap/gui/jmapviewer/MapMarkerCircle.java
@@ -1,106 +1,106 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.awt.AlphaComposite;
-import java.awt.Color;
-import java.awt.Composite;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-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 MapMarkerCircle extends MapObjectImpl implements MapMarker {
-
-    Coordinate coord;
-    double radius;
-    STYLE markerStyle;
-
-    public MapMarkerCircle(Coordinate coord, double radius) {
-        this(null, null, coord, radius);
-    }
-    public MapMarkerCircle(String name, Coordinate coord, double radius) {
-        this(null, name, coord, radius);
-    }
-    public MapMarkerCircle(Layer layer, Coordinate coord, double radius) {
-        this(layer, null, coord, radius);
-    }
-    public MapMarkerCircle(double lat, double lon, double radius) {
-        this(null, null, new Coordinate(lat,lon), radius);
-    }
-    public MapMarkerCircle(Layer layer, double lat, double lon, double radius) {
-        this(layer, null, new Coordinate(lat,lon), radius);
-    }
-    public MapMarkerCircle(Layer layer, String name, Coordinate coord, double radius) {
-        this(layer, name, coord, radius, STYLE.VARIABLE, getDefaultStyle());
-    }
-    public MapMarkerCircle(Layer layer, String name, Coordinate coord, double radius, STYLE markerStyle, Style style) {
-        super(layer, name, style);
-        this.markerStyle = markerStyle;
-        this.coord = coord;
-        this.radius = radius;
-    }
-
-    public Coordinate getCoordinate(){
-        return coord;
-    }
-    public double getLat() {
-        return coord.getLat();
-    }
-
-    public double getLon() {
-        return coord.getLon();
-    }
-
-    public double getRadius() {
-        return radius;
-    }
-
-    public STYLE getMarkerStyle() {
-        return markerStyle;
-    }
-
-    public void paint(Graphics g, Point position, int radio) {
-        int size_h = radio;
-        int size = size_h * 2;
-
-        if (g instanceof Graphics2D && getBackColor()!=null) {
-            Graphics2D g2 = (Graphics2D) g;
-            Composite oldComposite = g2.getComposite();
-            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
-            g2.setPaint(getBackColor());
-            g.fillOval(position.x - size_h, position.y - size_h, size, size);
-            g2.setComposite(oldComposite);
-        }
-        g.setColor(getColor());
-        g.drawOval(position.x - size_h, position.y - size_h, size, size);
-
-        if(getLayer()==null||getLayer().isVisibleTexts()) paintText(g, position);
-    }
-
-    public static Style getDefaultStyle(){
-        return new Style(Color.ORANGE, new Color(200,200,200,200), null, getDefaultFont());
-    }
-    @Override
-    public String toString() {
-        return "MapMarker at " + getLat() + " " + getLon();
-    }
-    @Override
-    public void setLat(double lat) {
-        if(coord==null) coord = new Coordinate(lat,0);
-        else coord.setLat(lat);
-    }
-    @Override
-    public void setLon(double lon) {
-        if(coord==null) coord = new Coordinate(0,lon);
-        else coord.setLon(lon);
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+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 MapMarkerCircle extends MapObjectImpl implements MapMarker {
+
+    Coordinate coord;
+    double radius;
+    STYLE markerStyle;
+
+    public MapMarkerCircle(Coordinate coord, double radius) {
+        this(null, null, coord, radius);
+    }
+    public MapMarkerCircle(String name, Coordinate coord, double radius) {
+        this(null, name, coord, radius);
+    }
+    public MapMarkerCircle(Layer layer, Coordinate coord, double radius) {
+        this(layer, null, coord, radius);
+    }
+    public MapMarkerCircle(double lat, double lon, double radius) {
+        this(null, null, new Coordinate(lat,lon), radius);
+    }
+    public MapMarkerCircle(Layer layer, double lat, double lon, double radius) {
+        this(layer, null, new Coordinate(lat,lon), radius);
+    }
+    public MapMarkerCircle(Layer layer, String name, Coordinate coord, double radius) {
+        this(layer, name, coord, radius, STYLE.VARIABLE, getDefaultStyle());
+    }
+    public MapMarkerCircle(Layer layer, String name, Coordinate coord, double radius, STYLE markerStyle, Style style) {
+        super(layer, name, style);
+        this.markerStyle = markerStyle;
+        this.coord = coord;
+        this.radius = radius;
+    }
+
+    public Coordinate getCoordinate(){
+        return coord;
+    }
+    public double getLat() {
+        return coord.getLat();
+    }
+
+    public double getLon() {
+        return coord.getLon();
+    }
+
+    public double getRadius() {
+        return radius;
+    }
+
+    public STYLE getMarkerStyle() {
+        return markerStyle;
+    }
+
+    public void paint(Graphics g, Point position, int radio) {
+        int size_h = radio;
+        int size = size_h * 2;
+
+        if (g instanceof Graphics2D && getBackColor()!=null) {
+            Graphics2D g2 = (Graphics2D) g;
+            Composite oldComposite = g2.getComposite();
+            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
+            g2.setPaint(getBackColor());
+            g.fillOval(position.x - size_h, position.y - size_h, size, size);
+            g2.setComposite(oldComposite);
+        }
+        g.setColor(getColor());
+        g.drawOval(position.x - size_h, position.y - size_h, size, size);
+
+        if(getLayer()==null||getLayer().isVisibleTexts()) paintText(g, position);
+    }
+
+    public static Style getDefaultStyle(){
+        return new Style(Color.ORANGE, new Color(200,200,200,200), null, getDefaultFont());
+    }
+    @Override
+    public String toString() {
+        return "MapMarker at " + getLat() + " " + getLon();
+    }
+    @Override
+    public void setLat(double lat) {
+        if(coord==null) coord = new Coordinate(lat,0);
+        else coord.setLat(lat);
+    }
+    @Override
+    public void setLon(double lon) {
+        if(coord==null) coord = new Coordinate(0,lon);
+        else coord.setLon(lon);
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java b/src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java
index b54e79a..1da1204 100644
--- a/src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java
+++ b/src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java
@@ -1,52 +1,52 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.awt.Color;
-
-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 extends MapMarkerCircle {
-
-    public static final int DOT_RADIUS = 5;
-
-    public MapMarkerDot(Coordinate coord) {
-        this(null, null, coord);
-    }
-    public MapMarkerDot(String name, Coordinate coord) {
-        this(null, name, coord);
-    }
-    public MapMarkerDot(Layer layer, Coordinate coord) {
-        this(layer, null, coord);
-    }
-    public MapMarkerDot(Layer layer, String name, Coordinate coord) {
-        this(layer, name, coord, getDefaultStyle());
-    }
-    public MapMarkerDot(Color color, double lat, double lon) {
-        this(null, null, lat, lon);
-        setColor(color);
-    }
-    public MapMarkerDot(double lat, double lon) {
-        this(null, null, lat, lon);
-    }
-    public MapMarkerDot(Layer layer, double lat, double lon) {
-        this(layer, null, lat, lon);
-    }
-    public MapMarkerDot(Layer layer, String name, double lat, double lon) {
-        this(layer, name, new Coordinate(lat, lon), getDefaultStyle());
-    }
-    public MapMarkerDot(Layer layer, String name, Coordinate coord, Style style) {
-        super(layer, name, coord, DOT_RADIUS, STYLE.FIXED, style);
-    }
-
-    public static Style getDefaultStyle(){
-        return new Style(Color.BLACK, Color.YELLOW, null, getDefaultFont());
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.awt.Color;
+
+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 extends MapMarkerCircle {
+
+    public static final int DOT_RADIUS = 5;
+
+    public MapMarkerDot(Coordinate coord) {
+        this(null, null, coord);
+    }
+    public MapMarkerDot(String name, Coordinate coord) {
+        this(null, name, coord);
+    }
+    public MapMarkerDot(Layer layer, Coordinate coord) {
+        this(layer, null, coord);
+    }
+    public MapMarkerDot(Layer layer, String name, Coordinate coord) {
+        this(layer, name, coord, getDefaultStyle());
+    }
+    public MapMarkerDot(Color color, double lat, double lon) {
+        this(null, null, lat, lon);
+        setColor(color);
+    }
+    public MapMarkerDot(double lat, double lon) {
+        this(null, null, lat, lon);
+    }
+    public MapMarkerDot(Layer layer, double lat, double lon) {
+        this(layer, null, lat, lon);
+    }
+    public MapMarkerDot(Layer layer, String name, double lat, double lon) {
+        this(layer, name, new Coordinate(lat, lon), getDefaultStyle());
+    }
+    public MapMarkerDot(Layer layer, String name, Coordinate coord, Style style) {
+        super(layer, name, coord, DOT_RADIUS, STYLE.FIXED, style);
+    }
+
+    public static Style getDefaultStyle(){
+        return new Style(Color.BLACK, Color.YELLOW, null, getDefaultFont());
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java b/src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java
index 220df59..9192029 100644
--- a/src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java
+++ b/src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java
@@ -1,226 +1,226 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.util.HashMap;
-import java.util.Map;
-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 final Map<String, CacheEntry> hash;
-
-    /**
-     * List of all tiles in their last recently used order
-     */
-    protected final CacheLinkedListElement lruTiles;
-
-    /**
-     * Constructs a new {@code MemoryTileCache}.
-     */
-    public MemoryTileCache() {
-        hash = new HashMap<>(cacheSize);
-        lruTiles = new CacheLinkedListElement();
-    }
-
-    @Override
-    public synchronized void addTile(Tile tile) {
-        CacheEntry entry = createCacheEntry(tile);
-            hash.put(tile.getKey(), entry);
-            lruTiles.addFirst(entry);
-            if (hash.size() > cacheSize) {
-                removeOldEntries();
-            }
-    }
-
-    @Override
-    public synchronized Tile getTile(TileSource source, int x, int y, int z) {
-        CacheEntry entry = hash.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 synchronized void removeOldEntries() {
-        try {
-            while (lruTiles.getElementCount() > cacheSize) {
-                removeEntry(lruTiles.getLastElement());
-            }
-        } catch (Exception e) {
-            log.warning(e.getMessage());
-        }
-    }
-
-    protected synchronized void removeEntry(CacheEntry entry) {
-        hash.remove(entry.tile.getKey());
-        lruTiles.removeEntry(entry);
-    }
-
-    protected CacheEntry createCacheEntry(Tile tile) {
-        return new CacheEntry(tile);
-    }
-
-    @Override
-    public synchronized void clear() {
-        hash.clear();
-        lruTiles.clear();
-    }
-
-    @Override
-    public int getTileCount() {
-        return hash.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 synchronized void setCacheSize(int cacheSize) {
-        this.cacheSize = cacheSize;
-        if (hash.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 void clear() {
-            elementCount = 0;
-            firstElement = null;
-            lastElement = null;
-        }
-
-        /**
-         * Add the element to the head of the list.
-         *
-         * @param element new element to be added
-         */
-        public void addFirst(CacheEntry element) {
-            if (element == null) return;
-            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 element from the list.
-         *
-         * @param element element to be removed
-         */
-        public void removeEntry(CacheEntry element) {
-            if (element == null) return;
-            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 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;
-        }
-
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.util.HashMap;
+import java.util.Map;
+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 final Map<String, CacheEntry> hash;
+
+    /**
+     * List of all tiles in their last recently used order
+     */
+    protected final CacheLinkedListElement lruTiles;
+
+    /**
+     * Constructs a new {@code MemoryTileCache}.
+     */
+    public MemoryTileCache() {
+        hash = new HashMap<>(cacheSize);
+        lruTiles = new CacheLinkedListElement();
+    }
+
+    @Override
+    public synchronized void addTile(Tile tile) {
+        CacheEntry entry = createCacheEntry(tile);
+            hash.put(tile.getKey(), entry);
+            lruTiles.addFirst(entry);
+            if (hash.size() > cacheSize) {
+                removeOldEntries();
+            }
+    }
+
+    @Override
+    public synchronized Tile getTile(TileSource source, int x, int y, int z) {
+        CacheEntry entry = hash.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 synchronized void removeOldEntries() {
+        try {
+            while (lruTiles.getElementCount() > cacheSize) {
+                removeEntry(lruTiles.getLastElement());
+            }
+        } catch (Exception e) {
+            log.warning(e.getMessage());
+        }
+    }
+
+    protected synchronized void removeEntry(CacheEntry entry) {
+        hash.remove(entry.tile.getKey());
+        lruTiles.removeEntry(entry);
+    }
+
+    protected CacheEntry createCacheEntry(Tile tile) {
+        return new CacheEntry(tile);
+    }
+
+    @Override
+    public synchronized void clear() {
+        hash.clear();
+        lruTiles.clear();
+    }
+
+    @Override
+    public int getTileCount() {
+        return hash.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 synchronized void setCacheSize(int cacheSize) {
+        this.cacheSize = cacheSize;
+        if (hash.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 void clear() {
+            elementCount = 0;
+            firstElement = null;
+            lastElement = null;
+        }
+
+        /**
+         * Add the element to the head of the list.
+         *
+         * @param element new element to be added
+         */
+        public void addFirst(CacheEntry element) {
+            if (element == null) return;
+            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 element from the list.
+         *
+         * @param element element to be removed
+         */
+        public void removeEntry(CacheEntry element) {
+            if (element == null) return;
+            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 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
index 08212ef..8291522 100644
--- a/src/org/openstreetmap/gui/jmapviewer/OsmFileCacheTileLoader.java
+++ b/src/org/openstreetmap/gui/jmapviewer/OsmFileCacheTileLoader.java
@@ -1,503 +1,504 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-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.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Random;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
-import org.openstreetmap.gui.jmapviewer.interfaces.TileClearController;
-import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
-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 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 implements CachedTileLoader {
-
-    private static final Logger log = FeatureAdapter.getLogger(OsmFileCacheTileLoader.class.getName());
-
-    protected 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 final Map<TileSource, File> sourceCacheDirMap;
-
-    protected long maxCacheFileAge = Long.MAX_VALUE;  // max. age not limited
-    protected long recheckAfter = FILE_AGE_ONE_WEEK;
-
-    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 the listener checking for tile load events (usually the map for display)
-     * @param cacheDir directory to store cached tiles
-     */
-    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();
-        sourceCacheDirMap = new HashMap<>();
-    }
-
-    /**
-     * Create a OSMFileCacheTileLoader with system property temp dir.
-     * If not set an IOException will be thrown.
-     * @param map the listener checking for tile load events (usually the map for display)
-     */
-    public OsmFileCacheTileLoader(TileLoaderListener map) throws SecurityException, IOException {
-        this(map, getDefaultCacheDir());
-    }
-
-    @Override
-    public TileJob createTileLoaderJob(final Tile tile) {
-        return new FileLoadJob(tile);
-    }
-
-    protected File getSourceCacheDir(TileSource source) {
-        File dir = sourceCacheDirMap.get(source);
-        if (dir == null) {
-            dir = new File(cacheDirBase, source.getName().replaceAll("[\\\\/:*?\"<>|]", "_"));
-            if (!dir.exists()) {
-                dir.mkdirs();
-            }
-        }
-        return dir;
-    }
-
-    protected class FileLoadJob implements TileJob {
-        InputStream input = null;
-
-        Tile tile;
-        File tileCacheDir;
-        File tileFile = null;
-        Long fileMtime = null;
-        Long now = null; // current time in milliseconds (keep consistent value for the whole run)
-
-        public FileLoadJob(Tile tile) {
-            this.tile = tile;
-        }
-
-        @Override
-        public Tile getTile() {
-            return tile;
-        }
-
-        @Override
-        public void run() {
-            synchronized (tile) {
-                if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading())
-                    return;
-                tile.loaded = false;
-                tile.error = false;
-                tile.loading = true;
-            }
-            now = System.currentTimeMillis();
-            tileCacheDir = getSourceCacheDir(tile.getSource());
-
-            if (loadTileFromFile(recheckAfter)) {
-                log.log(Level.FINE, "TMS - found in tile cache: {0}", tile);
-                tile.setLoaded(true);
-                listener.tileLoadingFinished(tile, true);
-                return;
-            }
-            TileJob job = new TileJob() {
-
-                @Override
-                public void run() {
-                    if (loadOrUpdateTile()) {
-                        tile.setLoaded(true);
-                        listener.tileLoadingFinished(tile, true);
-                    } else {
-                        // failed to download - use old cache file if available
-                        if (loadTileFromFile(maxCacheFileAge)) {
-                            tile.setLoaded(true);
-                            tile.error = false;
-                            listener.tileLoadingFinished(tile, true);
-                            log.log(Level.FINE, "TMS - found stale tile in cache: {0}", tile);
-                        } else {
-                            // failed completely
-                            tile.setLoaded(true);
-                            listener.tileLoadingFinished(tile, false);
-                        }
-                    }
-                }
-                @Override
-                public Tile getTile() {
-                    return tile;
-                }
-            };
-            JobDispatcher.getInstance().addJob(job);
-        }
-
-        protected boolean loadOrUpdateTile() {
-            try {
-                URLConnection urlConn = loadTileFromOsm(tile);
-                if (fileMtime != null && now - fileMtime <= maxCacheFileAge) {
-                    switch (tile.getSource().getTileUpdate()) {
-                    case IfModifiedSince:
-                        urlConn.setIfModifiedSince(fileMtime);
-                        break;
-                    case LastModified:
-                        if (!isOsmTileNewer(fileMtime)) {
-                            log.log(Level.FINE, "TMS - LastModified test: local version is up to date: {0}", tile);
-                            tileFile.setLastModified(now);
-                            return true;
-                        }
-                        break;
-                    default:
-                        break;
-                    }
-                }
-                if (tile.getSource().getTileUpdate() == TileUpdate.ETag || tile.getSource().getTileUpdate() == TileUpdate.IfNoneMatch) {
-                    String fileETag = tile.getValue("etag");
-                    if (fileETag != null) {
-                        switch (tile.getSource().getTileUpdate()) {
-                        case IfNoneMatch:
-                            urlConn.addRequestProperty("If-None-Match", fileETag);
-                            break;
-                        case ETag:
-                            if (hasOsmTileETag(fileETag)) {
-                                log.log(Level.FINE, "TMS - ETag test: local version is up to date: {0}", tile);
-                                tileFile.setLastModified(now);
-                                return true;
-                            }
-                        default:
-                            break;
-                        }
-                    }
-                    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"
-                    switch (tile.getSource().getTileUpdate()) {
-                    case IfModifiedSince:
-                        log.log(Level.FINE, "TMS - IfModifiedSince test: local version is up to date: {0}", tile);
-                        break;
-                    case IfNoneMatch:
-                        log.log(Level.FINE, "TMS - IfNoneMatch test: local version is up to date: {0}", tile);
-                        break;
-                    default:
-                        break;
-                    }
-                    if (loadTileFromFile(maxCacheFileAge)) {
-                        tileFile.setLastModified(now);
-                        return true;
-                    }
-                }
-
-                loadTileMetadata(tile, urlConn);
-                saveTagsToFile();
-
-                if ("no-tile".equals(tile.getValue("tile-info")))
-                {
-                    log.log(Level.FINE, "TMS - No tile: tile-info=no-tile: {0}", tile);
-                    tile.setError("No tile at this zoom level");
-                    return true;
-                } else {
-                    for (int i = 0; i < 5; ++i) {
-                        if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 503) {
-                            Thread.sleep(5000+(new Random()).nextInt(5000));
-                            continue;
-                        }
-                        byte[] buffer = loadTileInBuffer(urlConn);
-                        if (buffer != null) {
-                            tile.loadImage(new ByteArrayInputStream(buffer));
-                            saveTileToFile(buffer);
-                            log.log(Level.FINE, "TMS - downloaded tile from server: {0}", tile.getUrl());
-                            return true;
-                        }
-                    }
-                }
-            } catch (Exception e) {
-                tile.setError(e.getMessage());
-                if (input == null) {
-                    try {
-                        log.log(Level.WARNING, "TMS - Failed downloading {0}: {1}", new Object[]{tile.getUrl(), e.getMessage()});
-                        return false;
-                    } catch(IOException i) {
-                    }
-                }
-            }
-            log.log(Level.WARNING, "TMS - Failed downloading tile: {0}", tile);
-            return false;
-        }
-
-        protected boolean loadTileFromFile(long maxAge) {
-            try {
-                tileFile = getTileFile();
-                if (!tileFile.exists())
-                    return false;
-                loadTagsFromFile();
-
-                fileMtime = tileFile.lastModified();
-                if (now - fileMtime > maxAge)
-                    return false;
-
-                if ("no-tile".equals(tile.getValue("tile-info"))) {
-                    tile.setError("No tile at this zoom level");
-                    if (tileFile.exists()) {
-                        tileFile.delete();
-                    }
-                    tileFile = null;
-                } else {
-                    try (FileInputStream fin = new FileInputStream(tileFile)) {
-                        if (fin.available() == 0)
-                            throw new IOException("File empty");
-                        tile.loadImage(fin);
-                    }
-                }
-                return true;
-
-            } catch (Exception e) {
-                log.log(Level.WARNING, "TMS - Error while loading image from tile cache: {0}; {1}", new Object[]{e.getMessage(), tile});
-                tileFile.delete();
-                tileFile = null;
-                fileMtime = null;
-            }
-            return false;
-        }
-
-        protected byte[] loadTileInBuffer(URLConnection urlConn) throws IOException {
-            input = urlConn.getInputStream();
-            try {
-                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();
-            } finally {
-                input.close();
-                input = null;
-            }
-        }
-
-        /**
-         * Performs a <code>HEAD</code> request for retrieving the
-         * <code>LastModified</code> header value.
-         *
-         * Note: This does only work with servers providing the
-         * <code>LastModified</code> header:
-         * <ul>
-         * <li>{@link org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.CycleMap} - supported</li>
-         * <li>{@link org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.Mapnik} - not supported</li>
-         * </ul>
-         *
-         * @param fileAge time of the
-         * @return <code>true</code> 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(fileMtime));
-            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(fileMtime));
-            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() + "."
-                    + tile.getSource().getTileType());
-        }
-
-        protected File getTagsFile() {
-            return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "."
-                    + TAGS_FILE_EXT);
-        }
-
-        protected void saveTileToFile(byte[] rawData) {
-            File file = getTileFile();
-            file.getParentFile().mkdirs();
-            try (
-                FileOutputStream f = new FileOutputStream(file)
-            ) {
-                f.write(rawData);
-            } catch (Exception e) {
-                log.log(Level.SEVERE, "Failed to save tile content: {0}", e.getLocalizedMessage());
-            }
-        }
-
-        protected void saveTagsToFile() {
-            File tagsFile = getTagsFile();
-            if (tile.getMetadata() == null) {
-                tagsFile.delete();
-                return;
-            }
-            try (PrintWriter f = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tagsFile), TAGS_CHARSET))) {
-                for (Entry<String, String> entry : tile.getMetadata().entrySet()) {
-                    f.println(entry.getKey() + "=" + entry.getValue());
-                }
-            } catch (Exception e) {
-                System.err.println("Failed to save tile tags: " + e.getLocalizedMessage());
-            }
-        }
-
-        protected void loadTagsFromFile() {
-            File tagsFile = getTagsFile();
-            try (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));
-                }
-            } 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();
-    }
-
-    @Override
-    public void clearCache(TileSource source) {
-        clearCache(source, null);
-    }
-
-    @Override
-    public void clearCache(TileSource source, TileClearController controller) {
-        File dir = getSourceCacheDir(source);
-        if (dir != null) {
-            if (controller != null) controller.initClearDir(dir);
-            if (dir.isDirectory()) {
-                File[] files = dir.listFiles();
-                if (controller != null) controller.initClearFiles(files);
-                for (File file : files) {
-                    if (controller != null && controller.cancel()) return;
-                    file.delete();
-                    if (controller != null) controller.fileDeleted(file);
-                }
-            }
-            dir.delete();
-        }
-        if (controller != null) controller.clearFinished();
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+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.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileClearController;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
+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 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 implements CachedTileLoader {
+
+    private static final Logger log = FeatureAdapter.getLogger(OsmFileCacheTileLoader.class.getName());
+
+    protected 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 final Map<TileSource, File> sourceCacheDirMap;
+
+    protected long maxCacheFileAge = Long.MAX_VALUE;  // max. age not limited
+    protected long recheckAfter = FILE_AGE_ONE_WEEK;
+
+    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 the listener checking for tile load events (usually the map for display)
+     * @param cacheDir directory to store cached tiles
+     */
+    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();
+        sourceCacheDirMap = new HashMap<>();
+    }
+
+    /**
+     * Create a OSMFileCacheTileLoader with system property temp dir.
+     * If not set an IOException will be thrown.
+     * @param map the listener checking for tile load events (usually the map for display)
+     */
+    public OsmFileCacheTileLoader(TileLoaderListener map) throws SecurityException, IOException {
+        this(map, getDefaultCacheDir());
+    }
+
+    @Override
+    public TileJob createTileLoaderJob(final Tile tile) {
+        return new FileLoadJob(tile);
+    }
+
+    protected File getSourceCacheDir(TileSource source) {
+        File dir = sourceCacheDirMap.get(source);
+        if (dir == null) {
+            dir = new File(cacheDirBase, source.getName().replaceAll("[\\\\/:*?\"<>|]", "_"));
+            if (!dir.exists()) {
+                dir.mkdirs();
+            }
+        }
+        return dir;
+    }
+
+    protected class FileLoadJob implements TileJob {
+        InputStream input = null;
+
+        Tile tile;
+        File tileCacheDir;
+        File tileFile = null;
+        Long fileMtime = null;
+        Long now = null; // current time in milliseconds (keep consistent value for the whole run)
+
+        public FileLoadJob(Tile tile) {
+            this.tile = tile;
+        }
+
+        @Override
+        public Tile getTile() {
+            return tile;
+        }
+
+        @Override
+        public void run() {
+            synchronized (tile) {
+                if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading())
+                    return;
+                tile.loaded = false;
+                tile.error = false;
+                tile.loading = true;
+            }
+            now = System.currentTimeMillis();
+            tileCacheDir = getSourceCacheDir(tile.getSource());
+
+            if (loadTileFromFile(recheckAfter)) {
+                log.log(Level.FINE, "TMS - found in tile cache: {0}", tile);
+                tile.setLoaded(true);
+                listener.tileLoadingFinished(tile, true);
+                return;
+            }
+            TileJob job = new TileJob() {
+
+                @Override
+                public void run() {
+                    if (loadOrUpdateTile()) {
+                        tile.setLoaded(true);
+                        listener.tileLoadingFinished(tile, true);
+                    } else {
+                        // failed to download - use old cache file if available
+                        if (loadTileFromFile(maxCacheFileAge)) {
+                            tile.setLoaded(true);
+                            tile.error = false;
+                            listener.tileLoadingFinished(tile, true);
+                            log.log(Level.FINE, "TMS - found stale tile in cache: {0}", tile);
+                        } else {
+                            // failed completely
+                            tile.setLoaded(true);
+                            listener.tileLoadingFinished(tile, false);
+                        }
+                    }
+                }
+                @Override
+                public Tile getTile() {
+                    return tile;
+                }
+            };
+            JobDispatcher.getInstance().addJob(job);
+        }
+
+        protected boolean loadOrUpdateTile() {
+            try {
+                URLConnection urlConn = loadTileFromOsm(tile);
+                if (fileMtime != null && now - fileMtime <= maxCacheFileAge) {
+                    switch (tile.getSource().getTileUpdate()) {
+                    case IfModifiedSince:
+                        urlConn.setIfModifiedSince(fileMtime);
+                        break;
+                    case LastModified:
+                        if (!isOsmTileNewer(fileMtime)) {
+                            log.log(Level.FINE, "TMS - LastModified test: local version is up to date: {0}", tile);
+                            tileFile.setLastModified(now);
+                            return true;
+                        }
+                        break;
+                    default:
+                        break;
+                    }
+                }
+                if (tile.getSource().getTileUpdate() == TileUpdate.ETag || tile.getSource().getTileUpdate() == TileUpdate.IfNoneMatch) {
+                    String fileETag = tile.getValue("etag");
+                    if (fileETag != null) {
+                        switch (tile.getSource().getTileUpdate()) {
+                        case IfNoneMatch:
+                            urlConn.addRequestProperty("If-None-Match", fileETag);
+                            break;
+                        case ETag:
+                            if (hasOsmTileETag(fileETag)) {
+                                log.log(Level.FINE, "TMS - ETag test: local version is up to date: {0}", tile);
+                                tileFile.setLastModified(now);
+                                return true;
+                            }
+                        default:
+                            break;
+                        }
+                    }
+                    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"
+                    switch (tile.getSource().getTileUpdate()) {
+                    case IfModifiedSince:
+                        log.log(Level.FINE, "TMS - IfModifiedSince test: local version is up to date: {0}", tile);
+                        break;
+                    case IfNoneMatch:
+                        log.log(Level.FINE, "TMS - IfNoneMatch test: local version is up to date: {0}", tile);
+                        break;
+                    default:
+                        break;
+                    }
+                    if (loadTileFromFile(maxCacheFileAge)) {
+                        tileFile.setLastModified(now);
+                        return true;
+                    }
+                }
+
+                loadTileMetadata(tile, urlConn);
+                saveTagsToFile();
+
+                if ("no-tile".equals(tile.getValue("tile-info")))
+                {
+                    log.log(Level.FINE, "TMS - No tile: tile-info=no-tile: {0}", tile);
+                    tile.setError("No tile at this zoom level");
+                    return true;
+                } else {
+                    for (int i = 0; i < 5; ++i) {
+                        if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 503) {
+                            Thread.sleep(5000+(new Random()).nextInt(5000));
+                            continue;
+                        }
+                        byte[] buffer = loadTileInBuffer(urlConn);
+                        if (buffer != null) {
+                            tile.loadImage(new ByteArrayInputStream(buffer));
+                            saveTileToFile(buffer);
+                            log.log(Level.FINE, "TMS - downloaded tile from server: {0}", tile.getUrl());
+                            return true;
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                tile.setError(e.getMessage());
+                if (input == null) {
+                    try {
+                        log.log(Level.WARNING, "TMS - Failed downloading {0}: {1}", new Object[]{tile.getUrl(), e.getMessage()});
+                        return false;
+                    } catch(IOException i) {
+                    }
+                }
+            }
+            log.log(Level.WARNING, "TMS - Failed downloading tile: {0}", tile);
+            return false;
+        }
+
+        protected boolean loadTileFromFile(long maxAge) {
+            try {
+                tileFile = getTileFile();
+                if (!tileFile.exists())
+                    return false;
+                loadTagsFromFile();
+
+                fileMtime = tileFile.lastModified();
+                if (now - fileMtime > maxAge)
+                    return false;
+
+                if ("no-tile".equals(tile.getValue("tile-info"))) {
+                    tile.setError("No tile at this zoom level");
+                    if (tileFile.exists()) {
+                        tileFile.delete();
+                    }
+                    tileFile = null;
+                } else {
+                    try (FileInputStream fin = new FileInputStream(tileFile)) {
+                        if (fin.available() == 0)
+                            throw new IOException("File empty");
+                        tile.loadImage(fin);
+                    }
+                }
+                return true;
+
+            } catch (Exception e) {
+                log.log(Level.WARNING, "TMS - Error while loading image from tile cache: {0}; {1}", new Object[]{e.getMessage(), tile});
+                tileFile.delete();
+                tileFile = null;
+                fileMtime = null;
+            }
+            return false;
+        }
+
+        protected byte[] loadTileInBuffer(URLConnection urlConn) throws IOException {
+            input = urlConn.getInputStream();
+            try {
+                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();
+            } finally {
+                input.close();
+                input = null;
+            }
+        }
+
+        /**
+         * Performs a <code>HEAD</code> request for retrieving the
+         * <code>LastModified</code> header value.
+         *
+         * Note: This does only work with servers providing the
+         * <code>LastModified</code> header:
+         * <ul>
+         * <li>{@link org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.CycleMap} - supported</li>
+         * <li>{@link org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.Mapnik} - not supported</li>
+         * </ul>
+         *
+         * @param fileAge time of the
+         * @return <code>true</code> 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(fileMtime));
+            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(fileMtime));
+            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() + "."
+                    + tile.getSource().getTileType());
+        }
+
+        protected File getTagsFile() {
+            return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "."
+                    + TAGS_FILE_EXT);
+        }
+
+        protected void saveTileToFile(byte[] rawData) {
+            File file = getTileFile();
+            file.getParentFile().mkdirs();
+            try (
+                FileOutputStream f = new FileOutputStream(file)
+            ) {
+                f.write(rawData);
+            } catch (Exception e) {
+                log.log(Level.SEVERE, "Failed to save tile content: {0}", e.getLocalizedMessage());
+            }
+        }
+
+        protected void saveTagsToFile() {
+            File tagsFile = getTagsFile();
+            tagsFile.getParentFile().mkdirs();
+            if (tile.getMetadata() == null) {
+                tagsFile.delete();
+                return;
+            }
+            try (PrintWriter f = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tagsFile), TAGS_CHARSET))) {
+                for (Entry<String, String> entry : tile.getMetadata().entrySet()) {
+                    f.println(entry.getKey() + "=" + entry.getValue());
+                }
+            } catch (Exception e) {
+                System.err.println("Failed to save tile tags: " + e.getLocalizedMessage());
+            }
+        }
+
+        protected void loadTagsFromFile() {
+            File tagsFile = getTagsFile();
+            try (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));
+                }
+            } 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();
+    }
+
+    @Override
+    public void clearCache(TileSource source) {
+        clearCache(source, null);
+    }
+
+    @Override
+    public void clearCache(TileSource source, TileClearController controller) {
+        File dir = getSourceCacheDir(source);
+        if (dir != null) {
+            if (controller != null) controller.initClearDir(dir);
+            if (dir.isDirectory()) {
+                File[] files = dir.listFiles();
+                if (controller != null) controller.initClearFiles(files);
+                for (File file : files) {
+                    if (controller != null && controller.cancel()) return;
+                    file.delete();
+                    if (controller != null) controller.fileDeleted(file);
+                }
+            }
+            dir.delete();
+        }
+        if (controller != null) controller.clearFinished();
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java b/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
index 903ee25..f0a7bcd 100644
--- a/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
+++ b/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
@@ -1,174 +1,174 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-/**
- * 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.
- */
-public class OsmMercator {
-
-    public static int TILE_SIZE = 256;
-    public static final double MAX_LAT = 85.05112877980659;
-    public static final double MIN_LAT = -85.05112877980659;
-    private static double EARTH_RADIUS = 6378137; // equatorial earth radius for EPSG:3857 (Mercator)
-
-    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 zoom level to request pixel data
-     * @return number of pixels
-     */
-    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 pixelspace to coordinates and get the distance.
-     *
-     * @param x1 the first x coordinate
-     * @param y1 the first y coordinate
-     * @param x2 the second x coordinate
-     * @param y2 the second y coordinate
-     *
-     * @param zoomLevel the zoom level
-     * @return the distance
-     * @author Jason Huntley
-     */
-    public static double getDistance(int x1, int y1, int x2, int y2, int zoomLevel) {
-        double la1 = YToLat(y1, zoomLevel);
-        double lo1 = XToLon(x1, zoomLevel);
-        double la2 = YToLat(y2, zoomLevel);
-        double lo2 = XToLon(x2, zoomLevel);
-
-        return getDistance(la1, lo1, la2, lo2);
-    }
-
-    /**
-     * Gets the distance using Spherical law of cosines.
-     *
-     * @param la1 the Latitude in degrees
-     * @param lo1 the Longitude in degrees
-     * @param la2 the Latitude from 2nd coordinate in degrees
-     * @param lo2 the Longitude from 2nd coordinate in degrees
-     * @return the distance
-     * @author Jason Huntley
-     */
-    public static double getDistance(double la1, double lo1, double la2, double lo2) {
-        double aStartLat = Math.toRadians(la1);
-        double aStartLong = Math.toRadians(lo1);
-        double aEndLat =Math.toRadians(la2);
-        double aEndLong = Math.toRadians(lo2);
-
-        double distance = Math.acos(Math.sin(aStartLat) * Math.sin(aEndLat)
-                + Math.cos(aStartLat) * Math.cos(aEndLat)
-                * Math.cos(aEndLong - aStartLong));
-
-        return (EARTH_RADIUS * distance);
-    }
-
-    /**
-     * Transform longitude to pixelspace
-     *
-     * <p>
-     * Mathematical optimization<br>
-     * <code>
-     * x = radius(aZoomlevel) * toRadians(aLongitude) + falseEasting(aZoomLevel)<br>
-     * x = getMaxPixels(aZoomlevel) / (2 * PI) * (aLongitude * PI) / 180 + getMaxPixels(aZoomlevel) / 2<br>
-     * x = getMaxPixels(aZoomlevel) * aLongitude / 360 + 180 * getMaxPixels(aZoomlevel) / 360<br>
-     * x = getMaxPixels(aZoomlevel) * (aLongitude + 180) / 360<br>
-     * </code>
-     * </p>
-     *
-     * @param aLongitude
-     *            [-180..180]
-     * @return [0..2^Zoomlevel*TILE_SIZE[
-     * @author Jan Peter Stotz
-     */
-    public static double LonToX(double aLongitude, int aZoomlevel) {
-        int mp = getMaxPixels(aZoomlevel);
-        double x = (mp * (aLongitude + 180l)) / 360l;
-        return Math.min(x, mp - 1);
-    }
-
-    /**
-     * Transforms latitude to pixelspace
-     * <p>
-     * Mathematical optimization<br>
-     * <code>
-     * log(u) := log((1.0 + sin(toRadians(aLat))) / (1.0 - sin(toRadians(aLat))<br>
-     *
-     * y = -1 * (radius(aZoomlevel) / 2 * log(u)))) - falseNorthing(aZoomlevel))<br>
-     * y = -1 * (getMaxPixel(aZoomlevel) / 2 * PI / 2 * log(u)) - -1 * getMaxPixel(aZoomLevel) / 2<br>
-     * y = getMaxPixel(aZoomlevel) / (-4 * PI) * log(u)) + getMaxPixel(aZoomLevel) / 2<br>
-     * y = getMaxPixel(aZoomlevel) * ((log(u) / (-4 * PI)) + 1/2)<br>
-     * </code>
-     * </p>
-     * @param aLat
-     *            [-90...90]
-     * @return [0..2^Zoomlevel*TILE_SIZE[
-     * @author Jan Peter Stotz
-     */
-    public static double 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);
-        double y = mp * (0.5 - (log / (4.0 * Math.PI)));
-        return Math.min(y, mp - 1);
-    }
-
-    /**
-     * Transforms pixel coordinate X to longitude
-     *
-     * <p>
-     * Mathematical optimization<br>
-     * <code>
-     * lon = toDegree((aX - falseEasting(aZoomlevel)) / radius(aZoomlevel))<br>
-     * lon = 180 / PI * ((aX - getMaxPixels(aZoomlevel) / 2) / getMaxPixels(aZoomlevel) / (2 * PI)<br>
-     * lon = 180 * ((aX - getMaxPixels(aZoomlevel) / 2) / getMaxPixels(aZoomlevel))<br>
-     * lon = 360 / getMaxPixels(aZoomlevel) * (aX - getMaxPixels(aZoomlevel) / 2)<br>
-     * lon = 360 * aX / getMaxPixels(aZoomlevel) - 180<br>
-     * </code>
-     * </p>
-     * @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);
-    }
-
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+/**
+ * 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.
+ */
+public class OsmMercator {
+
+    public static int TILE_SIZE = 256;
+    public static final double MAX_LAT = 85.05112877980659;
+    public static final double MIN_LAT = -85.05112877980659;
+    private static double EARTH_RADIUS = 6378137; // equatorial earth radius for EPSG:3857 (Mercator)
+
+    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 zoom level to request pixel data
+     * @return number of pixels
+     */
+    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 pixelspace to coordinates and get the distance.
+     *
+     * @param x1 the first x coordinate
+     * @param y1 the first y coordinate
+     * @param x2 the second x coordinate
+     * @param y2 the second y coordinate
+     *
+     * @param zoomLevel the zoom level
+     * @return the distance
+     * @author Jason Huntley
+     */
+    public static double getDistance(int x1, int y1, int x2, int y2, int zoomLevel) {
+        double la1 = YToLat(y1, zoomLevel);
+        double lo1 = XToLon(x1, zoomLevel);
+        double la2 = YToLat(y2, zoomLevel);
+        double lo2 = XToLon(x2, zoomLevel);
+
+        return getDistance(la1, lo1, la2, lo2);
+    }
+
+    /**
+     * Gets the distance using Spherical law of cosines.
+     *
+     * @param la1 the Latitude in degrees
+     * @param lo1 the Longitude in degrees
+     * @param la2 the Latitude from 2nd coordinate in degrees
+     * @param lo2 the Longitude from 2nd coordinate in degrees
+     * @return the distance
+     * @author Jason Huntley
+     */
+    public static double getDistance(double la1, double lo1, double la2, double lo2) {
+        double aStartLat = Math.toRadians(la1);
+        double aStartLong = Math.toRadians(lo1);
+        double aEndLat =Math.toRadians(la2);
+        double aEndLong = Math.toRadians(lo2);
+
+        double distance = Math.acos(Math.sin(aStartLat) * Math.sin(aEndLat)
+                + Math.cos(aStartLat) * Math.cos(aEndLat)
+                * Math.cos(aEndLong - aStartLong));
+
+        return (EARTH_RADIUS * distance);
+    }
+
+    /**
+     * Transform longitude to pixelspace
+     *
+     * <p>
+     * Mathematical optimization<br>
+     * <code>
+     * x = radius(aZoomlevel) * toRadians(aLongitude) + falseEasting(aZoomLevel)<br>
+     * x = getMaxPixels(aZoomlevel) / (2 * PI) * (aLongitude * PI) / 180 + getMaxPixels(aZoomlevel) / 2<br>
+     * x = getMaxPixels(aZoomlevel) * aLongitude / 360 + 180 * getMaxPixels(aZoomlevel) / 360<br>
+     * x = getMaxPixels(aZoomlevel) * (aLongitude + 180) / 360<br>
+     * </code>
+     * </p>
+     *
+     * @param aLongitude
+     *            [-180..180]
+     * @return [0..2^Zoomlevel*TILE_SIZE[
+     * @author Jan Peter Stotz
+     */
+    public static double LonToX(double aLongitude, int aZoomlevel) {
+        int mp = getMaxPixels(aZoomlevel);
+        double x = (mp * (aLongitude + 180l)) / 360l;
+        return Math.min(x, mp - 1);
+    }
+
+    /**
+     * Transforms latitude to pixelspace
+     * <p>
+     * Mathematical optimization<br>
+     * <code>
+     * log(u) := log((1.0 + sin(toRadians(aLat))) / (1.0 - sin(toRadians(aLat))<br>
+     *
+     * y = -1 * (radius(aZoomlevel) / 2 * log(u)))) - falseNorthing(aZoomlevel))<br>
+     * y = -1 * (getMaxPixel(aZoomlevel) / 2 * PI / 2 * log(u)) - -1 * getMaxPixel(aZoomLevel) / 2<br>
+     * y = getMaxPixel(aZoomlevel) / (-4 * PI) * log(u)) + getMaxPixel(aZoomLevel) / 2<br>
+     * y = getMaxPixel(aZoomlevel) * ((log(u) / (-4 * PI)) + 1/2)<br>
+     * </code>
+     * </p>
+     * @param aLat
+     *            [-90...90]
+     * @return [0..2^Zoomlevel*TILE_SIZE[
+     * @author Jan Peter Stotz
+     */
+    public static double 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);
+        double y = mp * (0.5 - (log / (4.0 * Math.PI)));
+        return Math.min(y, mp - 1);
+    }
+
+    /**
+     * Transforms pixel coordinate X to longitude
+     *
+     * <p>
+     * Mathematical optimization<br>
+     * <code>
+     * lon = toDegree((aX - falseEasting(aZoomlevel)) / radius(aZoomlevel))<br>
+     * lon = 180 / PI * ((aX - getMaxPixels(aZoomlevel) / 2) / getMaxPixels(aZoomlevel) / (2 * PI)<br>
+     * lon = 180 * ((aX - getMaxPixels(aZoomlevel) / 2) / getMaxPixels(aZoomlevel))<br>
+     * lon = 360 / getMaxPixels(aZoomlevel) * (aX - getMaxPixels(aZoomlevel) / 2)<br>
+     * lon = 360 * aX / getMaxPixels(aZoomlevel) - 180<br>
+     * </code>
+     * </p>
+     * @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
index eee1fc8..fa0c170 100644
--- a/src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java
+++ b/src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java
@@ -1,128 +1,128 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLConnection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
-import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
-import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
-
-/**
- * A {@link TileLoader} implementation that loads tiles from OSM.
- *
- * @author Jan Peter Stotz
- */
-public class OsmTileLoader implements TileLoader {
-
-    /**
-     * Holds the HTTP headers. Insert e.g. User-Agent here when default should not be used.
-     */
-    public Map<String, String> headers = new HashMap<>();
-
-    public int timeoutConnect = 0;
-    public int timeoutRead = 0;
-
-    protected TileLoaderListener listener;
-
-    public OsmTileLoader(TileLoaderListener listener) {
-        headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
-        this.listener = listener;
-    }
-
-    public TileJob createTileLoaderJob(final Tile tile) {
-        return new TileJob() {
-
-            InputStream input = null;
-
-            public void run() {
-                synchronized (tile) {
-                    if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading())
-                        return;
-                    tile.loaded = false;
-                    tile.error = false;
-                    tile.loading = true;
-                }
-                try {
-                    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();
-                        try {
-                            tile.loadImage(input);
-                        } finally {
-                            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) {
-                        try {
-                            System.err.println("Failed loading " + tile.getUrl() +": "
-                                    +e.getClass() + ": " + e.getMessage());
-                        } catch (IOException ioe) {
-                            ioe.printStackTrace();
-                        }
-                    }
-                } finally {
-                    tile.loading = false;
-                    tile.setLoaded(true);
-                }
-            }
-
-            public Tile getTile() {
-                return tile;
-            }
-        };
-    }
-
-    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) {
-        for(Entry<String, String> e : headers.entrySet()) {
-            urlConn.setRequestProperty(e.getKey(), e.getValue());
-        }
-        if(timeoutConnect != 0)
-            urlConn.setConnectTimeout(timeoutConnect);
-        if(timeoutRead != 0)
-            urlConn.setReadTimeout(timeoutRead);
-    }
-
-    @Override
-    public String toString() {
-        return getClass().getSimpleName();
-    }
-
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+
+/**
+ * A {@link TileLoader} implementation that loads tiles from OSM.
+ *
+ * @author Jan Peter Stotz
+ */
+public class OsmTileLoader implements TileLoader {
+
+    /**
+     * Holds the HTTP headers. Insert e.g. User-Agent here when default should not be used.
+     */
+    public Map<String, String> headers = new HashMap<>();
+
+    public int timeoutConnect = 0;
+    public int timeoutRead = 0;
+
+    protected TileLoaderListener listener;
+
+    public OsmTileLoader(TileLoaderListener listener) {
+        headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
+        this.listener = listener;
+    }
+
+    public TileJob createTileLoaderJob(final Tile tile) {
+        return new TileJob() {
+
+            InputStream input = null;
+
+            public void run() {
+                synchronized (tile) {
+                    if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading())
+                        return;
+                    tile.loaded = false;
+                    tile.error = false;
+                    tile.loading = true;
+                }
+                try {
+                    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();
+                        try {
+                            tile.loadImage(input);
+                        } finally {
+                            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) {
+                        try {
+                            System.err.println("Failed loading " + tile.getUrl() +": "
+                                    +e.getClass() + ": " + e.getMessage());
+                        } catch (IOException ioe) {
+                            ioe.printStackTrace();
+                        }
+                    }
+                } finally {
+                    tile.loading = false;
+                    tile.setLoaded(true);
+                }
+            }
+
+            public Tile getTile() {
+                return tile;
+            }
+        };
+    }
+
+    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) {
+        for(Entry<String, String> e : headers.entrySet()) {
+            urlConn.setRequestProperty(e.getKey(), e.getValue());
+        }
+        if(timeoutConnect != 0)
+            urlConn.setConnectTimeout(timeoutConnect);
+        if(timeoutRead != 0)
+            urlConn.setReadTimeout(timeoutRead);
+    }
+
+    @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
index 82ab19e..895b962 100644
--- a/src/org/openstreetmap/gui/jmapviewer/Tile.java
+++ b/src/org/openstreetmap/gui/jmapviewer/Tile.java
@@ -1,334 +1,334 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer;
-
-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, except for overlay sources
-     */
-    public static BufferedImage LOADING_IMAGE;
-
-    /**
-     * Red cross image that is displayed after a loading error, except for overlay sources
-     */
-    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 ex) {
-            ex.printStackTrace();
-        }
-    }
-
-    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<String, String> metadata;
-
-    /**
-     * Creates a tile with empty image.
-     *
-     * @param source Tile source
-     * @param xtile X coordinate
-     * @param ytile Y coordinate
-     * @param zoom Zoom level
-     */
-    public Tile(TileSource source, int xtile, int ytile, int zoom) {
-        this(source, xtile, ytile, zoom, LOADING_IMAGE);
-    }
-
-    /**
-     * Creates a tile with specified image.
-     *
-     * @param source Tile source
-     * @param xtile X coordinate
-     * @param ytile Y coordinate
-     * @param zoom Zoom level
-     * @param image Image content
-     */
-    public Tile(TileSource source, int xtile, int ytile, int zoom, BufferedImage image) {
-        this.source = source;
-        this.xtile = xtile;
-        this.ytile = ytile;
-        this.zoom = zoom;
-        this.image = image;
-        this.key = getTileKey(source, xtile, ytile, zoom);
-    }
-
-    /**
-     * 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;
-    }
-
-    /**
-     * Returns the X coordinate.
-     * @return tile number on the x axis of this tile
-     */
-    public int getXtile() {
-        return xtile;
-    }
-
-    /**
-     * Returns the Y coordinate.
-     * @return tile number on the y axis of this tile
-     */
-    public int getYtile() {
-        return ytile;
-    }
-
-    /**
-     * Returns the zoom level.
-     * @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} <code>g</code> at the
-     * position <code>x</code>/<code>y</code>.
-     *
-     * @param g the Graphics object
-     * @param x x-coordinate in <code>g</code>
-     * @param y y-coordinate in <code>g</code>
-     */
-    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 <code>obj</code> 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;
-    }
-
-    /**
-     * Puts the given key/value pair to the metadata of the tile.
-     * If value is null, the (possibly existing) key/value pair is removed from
-     * the meta data.
-     *
-     * @param key Key
-     * @param value Value
-     */
-    public void putValue(String key, String value) {
-        if (value == null || value.isEmpty()) {
-            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<String,String> getMetadata() {
-        return metadata;
-    }
-
-    public void initLoading() {
-        loaded = false;
-        error = false;
-        loading = true;
-    }
-
-    public void finishLoading() {
-        loading = false;
-        loaded = true;
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+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, except for overlay sources
+     */
+    public static BufferedImage LOADING_IMAGE;
+
+    /**
+     * Red cross image that is displayed after a loading error, except for overlay sources
+     */
+    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 ex) {
+            ex.printStackTrace();
+        }
+    }
+
+    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<String, String> metadata;
+
+    /**
+     * Creates a tile with empty image.
+     *
+     * @param source Tile source
+     * @param xtile X coordinate
+     * @param ytile Y coordinate
+     * @param zoom Zoom level
+     */
+    public Tile(TileSource source, int xtile, int ytile, int zoom) {
+        this(source, xtile, ytile, zoom, LOADING_IMAGE);
+    }
+
+    /**
+     * Creates a tile with specified image.
+     *
+     * @param source Tile source
+     * @param xtile X coordinate
+     * @param ytile Y coordinate
+     * @param zoom Zoom level
+     * @param image Image content
+     */
+    public Tile(TileSource source, int xtile, int ytile, int zoom, BufferedImage image) {
+        this.source = source;
+        this.xtile = xtile;
+        this.ytile = ytile;
+        this.zoom = zoom;
+        this.image = image;
+        this.key = getTileKey(source, xtile, ytile, zoom);
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Returns the X coordinate.
+     * @return tile number on the x axis of this tile
+     */
+    public int getXtile() {
+        return xtile;
+    }
+
+    /**
+     * Returns the Y coordinate.
+     * @return tile number on the y axis of this tile
+     */
+    public int getYtile() {
+        return ytile;
+    }
+
+    /**
+     * Returns the zoom level.
+     * @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} <code>g</code> at the
+     * position <code>x</code>/<code>y</code>.
+     *
+     * @param g the Graphics object
+     * @param x x-coordinate in <code>g</code>
+     * @param y y-coordinate in <code>g</code>
+     */
+    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 <code>obj</code> 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;
+    }
+
+    /**
+     * Puts the given key/value pair to the metadata of the tile.
+     * If value is null, the (possibly existing) key/value pair is removed from
+     * the meta data.
+     *
+     * @param key Key
+     * @param value Value
+     */
+    public void putValue(String key, String value) {
+        if (value == null || value.isEmpty()) {
+            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<String,String> getMetadata() {
+        return metadata;
+    }
+
+    public void initLoading() {
+        loaded = false;
+        error = false;
+        loading = true;
+    }
+
+    public void finishLoading() {
+        loading = false;
+        loaded = true;
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/TileController.java b/src/org/openstreetmap/gui/jmapviewer/TileController.java
index d159ee1..ffde8ef 100644
--- a/src/org/openstreetmap/gui/jmapviewer/TileController.java
+++ b/src/org/openstreetmap/gui/jmapviewer/TileController.java
@@ -1,87 +1,87 @@
-// License: GPL. For details, see Readme.txt file.
-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;
-
-public class TileController {
-    protected TileLoader tileLoader;
-    protected TileCache tileCache;
-    protected TileSource tileSource;
-
-    JobDispatcher jobDispatcher;
-
-    public TileController(TileSource source, TileCache tileCache, TileLoaderListener listener) {
-        this.tileSource = source;
-        this.tileLoader = new OsmTileLoader(listener);
-        this.tileCache = tileCache;
-        this.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 the X position of the tile
-     * @param tiley the Y position of the tile
-     * @param zoom the zoom level of the tile
-     * @return specified tile from the cache or <code>null</code> 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.error) {
-            tile.loadPlaceholderFromCache(tileCache);
-        }
-        if (!tile.isLoaded()) {
-            jobDispatcher.addJob(tileLoader.createTileLoaderJob(tile));
-        }
-        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;
-    }
-
-    /**
-     * Removes all jobs from the queue that are currently not being processed.
-     */
-    public void cancelOutstandingJobs() {
-        jobDispatcher.cancelOutstandingJobs();
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+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;
+
+public class TileController {
+    protected TileLoader tileLoader;
+    protected TileCache tileCache;
+    protected TileSource tileSource;
+
+    JobDispatcher jobDispatcher;
+
+    public TileController(TileSource source, TileCache tileCache, TileLoaderListener listener) {
+        this.tileSource = source;
+        this.tileLoader = new OsmTileLoader(listener);
+        this.tileCache = tileCache;
+        this.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 the X position of the tile
+     * @param tiley the Y position of the tile
+     * @param zoom the zoom level of the tile
+     * @return specified tile from the cache or <code>null</code> 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.error) {
+            tile.loadPlaceholderFromCache(tileCache);
+        }
+        if (!tile.isLoaded()) {
+            jobDispatcher.addJob(tileLoader.createTileLoaderJob(tile));
+        }
+        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;
+    }
+
+    /**
+     * Removes all jobs from the queue that are currently not being processed.
+     */
+    public void cancelOutstandingJobs() {
+        jobDispatcher.cancelOutstandingJobs();
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/MapMarker.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/MapMarker.java
index 29ae5c2..2a1968f 100644
--- a/src/org/openstreetmap/gui/jmapviewer/interfaces/MapMarker.java
+++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/MapMarker.java
@@ -1,54 +1,54 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.interfaces;
-
-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 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 extends MapObject, ICoordinate{
-
-    public static enum STYLE {FIXED, VARIABLE}
-
-    /**
-     * @return Latitude and Longitude of the map marker position
-     */
-    public Coordinate getCoordinate();
-    /**
-     * @return Latitude of the map marker position
-     */
-    public double getLat();
-
-    /**
-     * @return Longitude of the map marker position
-     */
-    public double getLon();
-
-    /**
-     * @return Radius of the map marker position
-     */
-    public double getRadius();
-
-    /**
-     * @return Style of the map marker
-     */
-    public STYLE getMarkerStyle();
-
-    /**
-     * Paints the map marker on the map. The <code>position</code> specifies the
-     * coordinates within <code>g</code>
-     *
-     * @param g
-     * @param position
-     * @param radio
-     */
-    public void paint(Graphics g, Point position, int radio);
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+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 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 extends MapObject, ICoordinate{
+
+    public static enum STYLE {FIXED, VARIABLE}
+
+    /**
+     * @return Latitude and Longitude of the map marker position
+     */
+    public Coordinate getCoordinate();
+    /**
+     * @return Latitude of the map marker position
+     */
+    public double getLat();
+
+    /**
+     * @return Longitude of the map marker position
+     */
+    public double getLon();
+
+    /**
+     * @return Radius of the map marker position
+     */
+    public double getRadius();
+
+    /**
+     * @return Style of the map marker
+     */
+    public STYLE getMarkerStyle();
+
+    /**
+     * Paints the map marker on the map. The <code>position</code> specifies the
+     * coordinates within <code>g</code>
+     *
+     * @param g
+     * @param position
+     * @param radio
+     */
+    public void paint(Graphics g, Point position, int radio);
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java
index f76197b..0d11c11 100644
--- a/src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java
+++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java
@@ -1,38 +1,38 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.interfaces;
-
-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()
- */
-public interface MapRectangle extends MapObject{
-
-    /**
-     * @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 <code>topLeft</code> and
-     * <code>bottomRight</code> are specifying the coordinates within <code>g</code>
-     *
-     * @param g graphics structure for painting
-     * @param topLeft lop left edge of painting region
-     * @param bottomRight bottom right edge of painting region
-     */
-    public void paint(Graphics g, Point topLeft, Point bottomRight);
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+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()
+ */
+public interface MapRectangle extends MapObject{
+
+    /**
+     * @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 <code>topLeft</code> and
+     * <code>bottomRight</code> are specifying the coordinates within <code>g</code>
+     *
+     * @param g graphics structure for painting
+     * @param topLeft lop left edge of painting region
+     * @param bottomRight bottom right edge of painting region
+     */
+    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
index cda5d67..209d2fa 100644
--- a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java
+++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java
@@ -1,50 +1,50 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.interfaces;
-
-import org.openstreetmap.gui.jmapviewer.JMapViewer;
-import org.openstreetmap.gui.jmapviewer.Tile;
-
-/**
- * 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 <code>null</code>
-     * will be returned.
-     *
-     * @param source
-     *            the tile 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 <code>null</code> 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(TileSource, int, int, int)} is unspecified and depends on the
-     * implementation.
-     *
-     * @param tile the tile to be added
-     */
-    public void addTile(Tile tile);
-
-    /**
-     * @return the number of tiles hold by the cache
-     */
-    public int getTileCount();
-
-    /**
-     * Clears the cache deleting all tiles from memory.
-     */
-    public void clear();
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+import org.openstreetmap.gui.jmapviewer.JMapViewer;
+import org.openstreetmap.gui.jmapviewer.Tile;
+
+/**
+ * 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 <code>null</code>
+     * will be returned.
+     *
+     * @param source
+     *            the tile 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 <code>null</code> 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(TileSource, int, int, int)} is unspecified and depends on the
+     * implementation.
+     *
+     * @param tile the tile to be added
+     */
+    public void addTile(Tile tile);
+
+    /**
+     * @return the number of tiles hold by the cache
+     */
+    public int getTileCount();
+
+    /**
+     * Clears the cache deleting all tiles from memory.
+     */
+    public void clear();
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java
index 45fdc54..5a7cfb1 100644
--- a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java
+++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java
@@ -1,20 +1,20 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.interfaces;
-
-import org.openstreetmap.gui.jmapviewer.Tile;
-
-/**
- * Interface for implementing a tile loading job. Tiles are usually loaded via HTTP
- * or from a file.
- *
- * @author Dirk Stöcker
- */
-public interface TileJob extends Runnable {
-
-    /**
-     * Function to return the tile associated with the job
-     *
-     * @return {@link Tile} to be handled
-     */
-    public Tile getTile();
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+import org.openstreetmap.gui.jmapviewer.Tile;
+
+/**
+ * Interface for implementing a tile loading job. Tiles are usually loaded via HTTP
+ * or from a file.
+ *
+ * @author Dirk Stöcker
+ */
+public interface TileJob extends Runnable {
+
+    /**
+     * Function to return the tile associated with the job
+     *
+     * @return {@link Tile} to be handled
+     */
+    public Tile getTile();
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java
index c07619a..723d707 100644
--- a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java
+++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java
@@ -1,23 +1,23 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.interfaces;
-
-import org.openstreetmap.gui.jmapviewer.Tile;
-
-/**
- * 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 implementation of this function should create and return a
-     * new {@link TileJob} instance that performs the load action.
-     *
-     * @param tile the tile to be loaded
-     * @return {@link TileJob} implementation that performs the desired load
-     *          action.
-     */
-    public TileJob createTileLoaderJob(Tile tile);
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+import org.openstreetmap.gui.jmapviewer.Tile;
+
+/**
+ * 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 implementation of this function should create and return a
+     * new {@link TileJob} instance that performs the load action.
+     *
+     * @param tile the tile to be loaded
+     * @return {@link TileJob} implementation that performs the desired load
+     *          action.
+     */
+    public TileJob createTileLoaderJob(Tile tile);
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoaderListener.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoaderListener.java
index ecd566e..133c27a 100644
--- a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoaderListener.java
+++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoaderListener.java
@@ -1,16 +1,16 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.interfaces;
-
-import org.openstreetmap.gui.jmapviewer.Tile;
-
-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 The tile
-     * @param success {@code true} if the tile has been loaded successfully, {@code false} otherwise
-     */
-    public void tileLoadingFinished(Tile tile, boolean success);
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+import org.openstreetmap.gui.jmapviewer.Tile;
+
+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 The tile
+     * @param success {@code true} if the tile has been loaded successfully, {@code false} otherwise
+     */
+    public void tileLoadingFinished(Tile tile, boolean success);
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
index b8680e8..ab003a6 100644
--- a/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
@@ -1,158 +1,158 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.interfaces;
-
-import java.io.IOException;
-
-import org.openstreetmap.gui.jmapviewer.JMapViewer;
-
-/**
- *
- * @author Jan Peter Stotz
- */
-public interface TileSource extends Attributed {
-
-    /**
-     * Specifies the different mechanisms for detecting updated tiles
-     * respectively only download newer tiles than those stored locally.
-     *
-     * <ul>
-     * <li>{@link #IfNoneMatch} Server provides ETag header entry for all tiles
-     * and <b>supports</b> conditional download via <code>If-None-Match</code>
-     * header entry.</li>
-     * <li>{@link #ETag} Server provides ETag header entry for all tiles but
-     * <b>does not support</b> conditional download via
-     * <code>If-None-Match</code> header entry.</li>
-     * <li>{@link #IfModifiedSince} Server provides Last-Modified header entry
-     * for all tiles and <b>supports</b> conditional download via
-     * <code>If-Modified-Since</code> header entry.</li>
-     * <li>{@link #LastModified} Server provides Last-Modified header entry for
-     * all tiles but <b>does not support</b> conditional download via
-     * <code>If-Modified-Since</code> header entry.</li>
-     * <li>{@link #None} The server does not support any of the listed
-     * mechanisms.</li>
-     * </ul>
-     *
-     */
-    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}
-     */
-    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
-     */
-    int getMinZoom();
-
-    /**
-     * @return The supported tile update mechanism
-     * @see TileUpdate
-     */
-    TileUpdate getTileUpdate();
-
-    /**
-     * A tile layer name as displayed to the user.
-     *
-     * @return Name of the tile layer
-     */
-    String getName();
-
-    /**
-     * A unique id for this tile source.
-     * 
-     * Unlike the name it has to be unique and has to consist only of characters
-     * valid for filenames.
-     * 
-     * @return the id
-     */
-    String getId();
-
-    /**
-     * Constructs the tile url.
-     *
-     * @param zoom
-     * @param tilex
-     * @param tiley
-     * @return fully qualified url for downloading the specified tile image
-     */
-    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 <code>"png"</code>.
-     *
-     * @return file extension of the tile image type
-     */
-    String getTileType();
-
-    /**
-     * Specifies how large each tile is.
-     * @return The size of a single tile in pixels.
-     */
-    int getTileSize();
-
-    /**
-     * Gets the distance using Spherical law of cosines.
-     *  @return the distance, m.
-     */
-    double getDistance(double la1, double lo1, double la2, double lo2);
-
-    /**
-     * Transform longitude to pixelspace.
-     * @return [0..2^Zoomlevel*TILE_SIZE[
-     */
-    int LonToX(double aLongitude, int aZoomlevel);
-
-    /**
-     * Transforms latitude to pixelspace.
-     * @return [0..2^Zoomlevel*TILE_SIZE[
-     */
-    int LatToY(double aLat, int aZoomlevel);
-
-    /**
-     * Transforms pixel coordinate X to longitude
-     * @return ]-180..180[
-     */
-    double XToLon(int aX, int aZoomlevel);
-
-    /**
-     * Transforms pixel coordinate Y to latitude.
-     * @return [MIN_LAT..MAX_LAT]
-     */
-    double YToLat(int aY, int aZoomlevel);
-
-    /**
-     * Transforms longitude to X tile coordinate.
-     * @return [0..2^Zoomlevel[
-     */
-    double lonToTileX(double lon, int zoom);
-
-    /**
-     * Transforms latitude to Y tile coordinate.
-     * @return [0..2^Zoomlevel[
-     */
-    double latToTileY(double lat, int zoom);
-
-    /**
-     * Transforms tile X coordinate to longitude.
-     * @return ]-180..180[
-     */
-    double tileXToLon(int x, int zoom);
-
-    /**
-     * Transforms tile Y coordinate to latitude.
-     * @return [MIN_LAT..MAX_LAT]
-     */
-    double tileYToLat(int y, int zoom);
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+import java.io.IOException;
+
+import org.openstreetmap.gui.jmapviewer.JMapViewer;
+
+/**
+ *
+ * @author Jan Peter Stotz
+ */
+public interface TileSource extends Attributed {
+
+    /**
+     * Specifies the different mechanisms for detecting updated tiles
+     * respectively only download newer tiles than those stored locally.
+     *
+     * <ul>
+     * <li>{@link #IfNoneMatch} Server provides ETag header entry for all tiles
+     * and <b>supports</b> conditional download via <code>If-None-Match</code>
+     * header entry.</li>
+     * <li>{@link #ETag} Server provides ETag header entry for all tiles but
+     * <b>does not support</b> conditional download via
+     * <code>If-None-Match</code> header entry.</li>
+     * <li>{@link #IfModifiedSince} Server provides Last-Modified header entry
+     * for all tiles and <b>supports</b> conditional download via
+     * <code>If-Modified-Since</code> header entry.</li>
+     * <li>{@link #LastModified} Server provides Last-Modified header entry for
+     * all tiles but <b>does not support</b> conditional download via
+     * <code>If-Modified-Since</code> header entry.</li>
+     * <li>{@link #None} The server does not support any of the listed
+     * mechanisms.</li>
+     * </ul>
+     *
+     */
+    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}
+     */
+    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
+     */
+    int getMinZoom();
+
+    /**
+     * @return The supported tile update mechanism
+     * @see TileUpdate
+     */
+    TileUpdate getTileUpdate();
+
+    /**
+     * A tile layer name as displayed to the user.
+     *
+     * @return Name of the tile layer
+     */
+    String getName();
+
+    /**
+     * A unique id for this tile source.
+     * 
+     * Unlike the name it has to be unique and has to consist only of characters
+     * valid for filenames.
+     * 
+     * @return the id
+     */
+    String getId();
+
+    /**
+     * Constructs the tile url.
+     *
+     * @param zoom
+     * @param tilex
+     * @param tiley
+     * @return fully qualified url for downloading the specified tile image
+     */
+    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 <code>"png"</code>.
+     *
+     * @return file extension of the tile image type
+     */
+    String getTileType();
+
+    /**
+     * Specifies how large each tile is.
+     * @return The size of a single tile in pixels.
+     */
+    int getTileSize();
+
+    /**
+     * Gets the distance using Spherical law of cosines.
+     *  @return the distance, m.
+     */
+    double getDistance(double la1, double lo1, double la2, double lo2);
+
+    /**
+     * Transform longitude to pixelspace.
+     * @return [0..2^Zoomlevel*TILE_SIZE[
+     */
+    int LonToX(double aLongitude, int aZoomlevel);
+
+    /**
+     * Transforms latitude to pixelspace.
+     * @return [0..2^Zoomlevel*TILE_SIZE[
+     */
+    int LatToY(double aLat, int aZoomlevel);
+
+    /**
+     * Transforms pixel coordinate X to longitude
+     * @return ]-180..180[
+     */
+    double XToLon(int aX, int aZoomlevel);
+
+    /**
+     * Transforms pixel coordinate Y to latitude.
+     * @return [MIN_LAT..MAX_LAT]
+     */
+    double YToLat(int aY, int aZoomlevel);
+
+    /**
+     * Transforms longitude to X tile coordinate.
+     * @return [0..2^Zoomlevel[
+     */
+    double lonToTileX(double lon, int zoom);
+
+    /**
+     * Transforms latitude to Y tile coordinate.
+     * @return [0..2^Zoomlevel[
+     */
+    double latToTileY(double lat, int zoom);
+
+    /**
+     * Transforms tile X coordinate to longitude.
+     * @return ]-180..180[
+     */
+    double tileXToLon(int x, int zoom);
+
+    /**
+     * Transforms tile Y coordinate to latitude.
+     * @return [MIN_LAT..MAX_LAT]
+     */
+    double tileYToLat(int y, int zoom);
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
index e3cfd19..2ec140b 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
@@ -1,125 +1,125 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.tilesources;
-
-import java.io.IOException;
-
-import org.openstreetmap.gui.jmapviewer.OsmMercator;
-
-public abstract class AbstractTMSTileSource extends AbstractTileSource {
-
-    protected String name;
-    protected String baseUrl;
-    protected String id;
-
-    public AbstractTMSTileSource(String name, String base_url, String id) {
-        this.name = name;
-        this.baseUrl = base_url;
-        if(baseUrl.endsWith("/")) {
-            baseUrl = baseUrl.substring(0,baseUrl.length()-1);
-        }
-        this.id = id;
-    }
-
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    @Override
-    public String getId() {
-        return id;
-    }
-
-    @Override
-    public int getMaxZoom() {
-        return 21;
-    }
-
-    @Override
-    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;
-    }
-
-    @Override
-    public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
-        return this.getBaseUrl() + getTilePath(zoom, tilex, tiley);
-    }
-
-    @Override
-    public String toString() {
-        return getName();
-    }
-
-    @Override
-    public String getTileType() {
-        return "png";
-    }
-
-    /*
-     * Most tilesources use OsmMercator projection.
-     */
-    @Override
-    public int getTileSize() {
-        return OsmMercator.TILE_SIZE;
-    }
-
-    @Override
-    public double getDistance(double lat1, double lon1, double lat2, double lon2) {
-        return OsmMercator.getDistance(lat1, lon1, lat2, lon2);
-    }
-
-    @Override
-    public int LonToX(double lon, int zoom) {
-        return (int )OsmMercator.LonToX(lon, zoom);
-    }
-
-    @Override
-    public int LatToY(double lat, int zoom) {
-        return (int )OsmMercator.LatToY(lat, zoom);
-    }
-
-    @Override
-    public double XToLon(int x, int zoom) {
-        return OsmMercator.XToLon(x, zoom);
-    }
-
-    @Override
-    public double YToLat(int y, int zoom) {
-        return OsmMercator.YToLat(y, zoom);
-    }
-
-    @Override
-    public double latToTileY(double lat, int zoom) {
-        return OsmMercator.LatToY(lat, zoom) / OsmMercator.TILE_SIZE;
-    }
-
-    @Override
-    public double lonToTileX(double lon, int zoom) {
-        return OsmMercator.LonToX(lon, zoom) / OsmMercator.TILE_SIZE;
-    }
-
-    @Override
-    public double tileYToLat(int y, int zoom) {
-        return OsmMercator.YToLat(y * OsmMercator.TILE_SIZE, zoom);
-    }
-
-    @Override
-    public double tileXToLon(int x, int zoom) {
-        return OsmMercator.XToLon(x * OsmMercator.TILE_SIZE, zoom);
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+import java.io.IOException;
+
+import org.openstreetmap.gui.jmapviewer.OsmMercator;
+
+public abstract class AbstractTMSTileSource extends AbstractTileSource {
+
+    protected String name;
+    protected String baseUrl;
+    protected String id;
+
+    public AbstractTMSTileSource(String name, String base_url, String id) {
+        this.name = name;
+        this.baseUrl = base_url;
+        if(baseUrl.endsWith("/")) {
+            baseUrl = baseUrl.substring(0,baseUrl.length()-1);
+        }
+        this.id = id;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String getId() {
+        return id;
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return 21;
+    }
+
+    @Override
+    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;
+    }
+
+    @Override
+    public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
+        return this.getBaseUrl() + getTilePath(zoom, tilex, tiley);
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+    @Override
+    public String getTileType() {
+        return "png";
+    }
+
+    /*
+     * Most tilesources use OsmMercator projection.
+     */
+    @Override
+    public int getTileSize() {
+        return OsmMercator.TILE_SIZE;
+    }
+
+    @Override
+    public double getDistance(double lat1, double lon1, double lat2, double lon2) {
+        return OsmMercator.getDistance(lat1, lon1, lat2, lon2);
+    }
+
+    @Override
+    public int LonToX(double lon, int zoom) {
+        return (int )OsmMercator.LonToX(lon, zoom);
+    }
+
+    @Override
+    public int LatToY(double lat, int zoom) {
+        return (int )OsmMercator.LatToY(lat, zoom);
+    }
+
+    @Override
+    public double XToLon(int x, int zoom) {
+        return OsmMercator.XToLon(x, zoom);
+    }
+
+    @Override
+    public double YToLat(int y, int zoom) {
+        return OsmMercator.YToLat(y, zoom);
+    }
+
+    @Override
+    public double latToTileY(double lat, int zoom) {
+        return OsmMercator.LatToY(lat, zoom) / OsmMercator.TILE_SIZE;
+    }
+
+    @Override
+    public double lonToTileX(double lon, int zoom) {
+        return OsmMercator.LonToX(lon, zoom) / OsmMercator.TILE_SIZE;
+    }
+
+    @Override
+    public double tileYToLat(int y, int zoom) {
+        return OsmMercator.YToLat(y * OsmMercator.TILE_SIZE, zoom);
+    }
+
+    @Override
+    public double tileXToLon(int x, int zoom) {
+        return OsmMercator.XToLon(x * OsmMercator.TILE_SIZE, zoom);
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java
index f8723b6..e71d093 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java
@@ -1,305 +1,305 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.tilesources;
-
-import java.awt.Image;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.regex.Pattern;
-
-import javax.imageio.ImageIO;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathConstants;
-import javax.xml.xpath.XPathExpression;
-import javax.xml.xpath.XPathExpressionException;
-import javax.xml.xpath.XPathFactory;
-
-import org.openstreetmap.gui.jmapviewer.Coordinate;
-import org.openstreetmap.gui.jmapviewer.JMapViewer;
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-public class BingAerialTileSource extends AbstractTMSTileSource {
-
-    private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
-    private static volatile Future<List<Attribution>> attributions; // volatile is required for getAttribution(), see below.
-    private static String imageUrlTemplate;
-    private static Integer imageryZoomMax;
-    private static String[] subdomains;
-
-    private static final Pattern subdomainPattern = Pattern.compile("\\{subdomain\\}");
-    private static final Pattern quadkeyPattern = Pattern.compile("\\{quadkey\\}");
-    private static final Pattern culturePattern = Pattern.compile("\\{culture\\}");
-    private String brandLogoUri = null;
-
-    /**
-     * Constructs a new {@code BingAerialTileSource}.
-     */
-    public BingAerialTileSource() {
-        this("Bing");
-    }
-
-    /**
-     * Constructs a new {@code BingAerialTileSource}.
-     */
-    public BingAerialTileSource(String id) {
-        super("Bing Aerial Maps", "http://example.com/", id);
-    }
-
-    protected class Attribution {
-        String attribution;
-        int minZoom;
-        int maxZoom;
-        Coordinate min;
-        Coordinate max;
-    }
-
-    @Override
-    public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
-        // make sure that attribution is loaded. otherwise subdomains is null.
-        if (getAttribution() == null)
-            throw new IOException("Attribution is not loaded yet");
-
-        int t = (zoom + tilex + tiley) % subdomains.length;
-        String subdomain = subdomains[t];
-
-        String url = imageUrlTemplate;
-        url = subdomainPattern.matcher(url).replaceAll(subdomain);
-        url = quadkeyPattern.matcher(url).replaceAll(computeQuadTree(zoom, tilex, tiley));
-
-        return url;
-    }
-
-    protected URL getAttributionUrl() throws MalformedURLException {
-        return new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&output=xml&key="
-                + API_KEY);
-    }
-
-    protected List<Attribution> parseAttributionText(InputSource xml) throws IOException {
-        try {
-            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-            DocumentBuilder builder = factory.newDocumentBuilder();
-            Document document = builder.parse(xml);
-
-            XPathFactory xPathFactory = XPathFactory.newInstance();
-            XPath xpath = xPathFactory.newXPath();
-            imageUrlTemplate = xpath.compile("//ImageryMetadata/ImageUrl/text()").evaluate(document);
-            imageUrlTemplate = culturePattern.matcher(imageUrlTemplate).replaceAll(Locale.getDefault().toString());
-            imageryZoomMax = Integer.parseInt(xpath.compile("//ImageryMetadata/ZoomMax/text()").evaluate(document));
-
-            NodeList subdomainTxt = (NodeList) xpath.compile("//ImageryMetadata/ImageUrlSubdomains/string/text()").evaluate(document, XPathConstants.NODESET);
-            subdomains = new String[subdomainTxt.getLength()];
-            for(int i = 0; i < subdomainTxt.getLength(); i++) {
-                subdomains[i] = subdomainTxt.item(i).getNodeValue();
-            }
-
-            brandLogoUri = xpath.compile("/Response/BrandLogoUri/text()").evaluate(document);
-
-            XPathExpression attributionXpath = xpath.compile("Attribution/text()");
-            XPathExpression coverageAreaXpath = xpath.compile("CoverageArea");
-            XPathExpression zoomMinXpath = xpath.compile("ZoomMin/text()");
-            XPathExpression zoomMaxXpath = xpath.compile("ZoomMax/text()");
-            XPathExpression southLatXpath = xpath.compile("BoundingBox/SouthLatitude/text()");
-            XPathExpression westLonXpath = xpath.compile("BoundingBox/WestLongitude/text()");
-            XPathExpression northLatXpath = xpath.compile("BoundingBox/NorthLatitude/text()");
-            XPathExpression eastLonXpath = xpath.compile("BoundingBox/EastLongitude/text()");
-
-            NodeList imageryProviderNodes = (NodeList) xpath.compile("//ImageryMetadata/ImageryProvider").evaluate(document, XPathConstants.NODESET);
-            List<Attribution> attributions = new ArrayList<>(imageryProviderNodes.getLength());
-            for (int i = 0; i < imageryProviderNodes.getLength(); i++) {
-                Node providerNode = imageryProviderNodes.item(i);
-
-                String attribution = attributionXpath.evaluate(providerNode);
-
-                NodeList coverageAreaNodes = (NodeList) coverageAreaXpath.evaluate(providerNode, XPathConstants.NODESET);
-                for(int j = 0; j < coverageAreaNodes.getLength(); j++) {
-                    Node areaNode = coverageAreaNodes.item(j);
-                    Attribution attr = new Attribution();
-                    attr.attribution = attribution;
-
-                    attr.maxZoom = Integer.parseInt(zoomMaxXpath.evaluate(areaNode));
-                    attr.minZoom = Integer.parseInt(zoomMinXpath.evaluate(areaNode));
-
-                    Double southLat = Double.parseDouble(southLatXpath.evaluate(areaNode));
-                    Double northLat = Double.parseDouble(northLatXpath.evaluate(areaNode));
-                    Double westLon = Double.parseDouble(westLonXpath.evaluate(areaNode));
-                    Double eastLon = Double.parseDouble(eastLonXpath.evaluate(areaNode));
-                    attr.min = new Coordinate(southLat, westLon);
-                    attr.max = new Coordinate(northLat, eastLon);
-
-                    attributions.add(attr);
-                }
-            }
-
-            return attributions;
-        } catch (SAXException e) {
-            System.err.println("Could not parse Bing aerials attribution metadata.");
-            e.printStackTrace();
-        } catch (ParserConfigurationException e) {
-            e.printStackTrace();
-        } catch (XPathExpressionException e) {
-            e.printStackTrace();
-        }
-        return null;
-    }
-
-    @Override
-    public int getMaxZoom() {
-        if(imageryZoomMax != null)
-            return imageryZoomMax;
-        else
-            return 22;
-    }
-
-    @Override
-    public TileUpdate getTileUpdate() {
-        return TileUpdate.IfNoneMatch;
-    }
-
-    @Override
-    public boolean requiresAttribution() {
-        return true;
-    }
-
-    @Override
-    public String getAttributionLinkURL() {
-        //return "http://bing.com/maps"
-        // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU
-        // (the requirement is that we have such a link at the bottom of the window)
-        return "http://go.microsoft.com/?linkid=9710837";
-    }
-
-    @Override
-    public Image getAttributionImage() {
-        try {
-            final InputStream imageResource = JMapViewer.class.getResourceAsStream("images/bing_maps.png");
-            if (imageResource != null) {
-                return ImageIO.read(imageResource);
-            } else {
-                // Some Linux distributions (like Debian) will remove Bing logo from sources, so get it at runtime
-                for (int i = 0; i < 5 && getAttribution() == null; i++) {
-                    // Makes sure attribution is loaded
-                }
-                if (brandLogoUri != null && !brandLogoUri.isEmpty()) {
-                    System.out.println("Reading Bing logo from "+brandLogoUri);
-                    return ImageIO.read(new URL(brandLogoUri));
-                }
-            }
-        } catch (IOException e) {
-            System.err.println("Error while retrieving Bing logo: "+e.getMessage());
-        }
-        return null;
-    }
-
-    @Override
-    public String getAttributionImageURL() {
-        return "http://opengeodata.org/microsoft-imagery-details";
-    }
-
-    @Override
-    public String getTermsOfUseText() {
-        return null;
-    }
-
-    @Override
-    public String getTermsOfUseURL() {
-        return "http://opengeodata.org/microsoft-imagery-details";
-    }
-
-    protected Callable<List<Attribution>> getAttributionLoaderCallable() {
-        return new Callable<List<Attribution>>() {
-
-            @Override
-            public List<Attribution> call() throws Exception {
-                int waitTimeSec = 1;
-                while (true) {
-                    try {
-                        InputSource xml = new InputSource(getAttributionUrl().openStream());
-                        List<Attribution> r = parseAttributionText(xml);
-                        System.out.println("Successfully loaded Bing attribution data.");
-                        return r;
-                    } catch (IOException ex) {
-                        System.err.println("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
-                        Thread.sleep(waitTimeSec * 1000L);
-                        waitTimeSec *= 2;
-                    }
-                }
-            }
-        };
-    }
-
-    protected List<Attribution> getAttribution() {
-        if (attributions == null) {
-            // see http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
-            synchronized (BingAerialTileSource.class) {
-                if (attributions == null) {
-                    attributions = Executors.newSingleThreadExecutor().submit(getAttributionLoaderCallable());
-                }
-            }
-        }
-        try {
-            return attributions.get(1000, TimeUnit.MILLISECONDS);
-        } catch (TimeoutException ex) {
-            System.err.println("Bing: attribution data is not yet loaded.");
-        } catch (ExecutionException ex) {
-            throw new RuntimeException(ex.getCause());
-        } catch (InterruptedException ign) {
-        }
-        return null;
-    }
-
-    @Override
-    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
-        try {
-            final List<Attribution> data = getAttribution();
-            if (data == null)
-                return "Error loading Bing attribution data";
-            StringBuilder a = new StringBuilder();
-            for (Attribution attr : data) {
-                if (zoom <= attr.maxZoom && zoom >= attr.minZoom) {
-                    if (topLeft.getLon() < attr.max.getLon() && botRight.getLon() > attr.min.getLon()
-                            && topLeft.getLat() > attr.min.getLat() && botRight.getLat() < attr.max.getLat()) {
-                        a.append(attr.attribution);
-                        a.append(" ");
-                    }
-                }
-            }
-            return a.toString();
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        return "Error loading Bing attribution data";
-    }
-
-    static String computeQuadTree(int zoom, int tilex, int tiley) {
-        StringBuilder k = new StringBuilder();
-        for (int i = zoom; i > 0; i--) {
-            char digit = 48;
-            int mask = 1 << (i - 1);
-            if ((tilex & mask) != 0) {
-                digit += 1;
-            }
-            if ((tiley & mask) != 0) {
-                digit += 2;
-            }
-            k.append(digit);
-        }
-        return k.toString();
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+import java.awt.Image;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Pattern;
+
+import javax.imageio.ImageIO;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.openstreetmap.gui.jmapviewer.Coordinate;
+import org.openstreetmap.gui.jmapviewer.JMapViewer;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class BingAerialTileSource extends AbstractTMSTileSource {
+
+    private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
+    private static volatile Future<List<Attribution>> attributions; // volatile is required for getAttribution(), see below.
+    private static String imageUrlTemplate;
+    private static Integer imageryZoomMax;
+    private static String[] subdomains;
+
+    private static final Pattern subdomainPattern = Pattern.compile("\\{subdomain\\}");
+    private static final Pattern quadkeyPattern = Pattern.compile("\\{quadkey\\}");
+    private static final Pattern culturePattern = Pattern.compile("\\{culture\\}");
+    private String brandLogoUri = null;
+
+    /**
+     * Constructs a new {@code BingAerialTileSource}.
+     */
+    public BingAerialTileSource() {
+        this("Bing");
+    }
+
+    /**
+     * Constructs a new {@code BingAerialTileSource}.
+     */
+    public BingAerialTileSource(String id) {
+        super("Bing Aerial Maps", "http://example.com/", id);
+    }
+
+    protected class Attribution {
+        String attribution;
+        int minZoom;
+        int maxZoom;
+        Coordinate min;
+        Coordinate max;
+    }
+
+    @Override
+    public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
+        // make sure that attribution is loaded. otherwise subdomains is null.
+        if (getAttribution() == null)
+            throw new IOException("Attribution is not loaded yet");
+
+        int t = (zoom + tilex + tiley) % subdomains.length;
+        String subdomain = subdomains[t];
+
+        String url = imageUrlTemplate;
+        url = subdomainPattern.matcher(url).replaceAll(subdomain);
+        url = quadkeyPattern.matcher(url).replaceAll(computeQuadTree(zoom, tilex, tiley));
+
+        return url;
+    }
+
+    protected URL getAttributionUrl() throws MalformedURLException {
+        return new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&output=xml&key="
+                + API_KEY);
+    }
+
+    protected List<Attribution> parseAttributionText(InputSource xml) throws IOException {
+        try {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document document = builder.parse(xml);
+
+            XPathFactory xPathFactory = XPathFactory.newInstance();
+            XPath xpath = xPathFactory.newXPath();
+            imageUrlTemplate = xpath.compile("//ImageryMetadata/ImageUrl/text()").evaluate(document);
+            imageUrlTemplate = culturePattern.matcher(imageUrlTemplate).replaceAll(Locale.getDefault().toString());
+            imageryZoomMax = Integer.parseInt(xpath.compile("//ImageryMetadata/ZoomMax/text()").evaluate(document));
+
+            NodeList subdomainTxt = (NodeList) xpath.compile("//ImageryMetadata/ImageUrlSubdomains/string/text()").evaluate(document, XPathConstants.NODESET);
+            subdomains = new String[subdomainTxt.getLength()];
+            for(int i = 0; i < subdomainTxt.getLength(); i++) {
+                subdomains[i] = subdomainTxt.item(i).getNodeValue();
+            }
+
+            brandLogoUri = xpath.compile("/Response/BrandLogoUri/text()").evaluate(document);
+
+            XPathExpression attributionXpath = xpath.compile("Attribution/text()");
+            XPathExpression coverageAreaXpath = xpath.compile("CoverageArea");
+            XPathExpression zoomMinXpath = xpath.compile("ZoomMin/text()");
+            XPathExpression zoomMaxXpath = xpath.compile("ZoomMax/text()");
+            XPathExpression southLatXpath = xpath.compile("BoundingBox/SouthLatitude/text()");
+            XPathExpression westLonXpath = xpath.compile("BoundingBox/WestLongitude/text()");
+            XPathExpression northLatXpath = xpath.compile("BoundingBox/NorthLatitude/text()");
+            XPathExpression eastLonXpath = xpath.compile("BoundingBox/EastLongitude/text()");
+
+            NodeList imageryProviderNodes = (NodeList) xpath.compile("//ImageryMetadata/ImageryProvider").evaluate(document, XPathConstants.NODESET);
+            List<Attribution> attributions = new ArrayList<>(imageryProviderNodes.getLength());
+            for (int i = 0; i < imageryProviderNodes.getLength(); i++) {
+                Node providerNode = imageryProviderNodes.item(i);
+
+                String attribution = attributionXpath.evaluate(providerNode);
+
+                NodeList coverageAreaNodes = (NodeList) coverageAreaXpath.evaluate(providerNode, XPathConstants.NODESET);
+                for(int j = 0; j < coverageAreaNodes.getLength(); j++) {
+                    Node areaNode = coverageAreaNodes.item(j);
+                    Attribution attr = new Attribution();
+                    attr.attribution = attribution;
+
+                    attr.maxZoom = Integer.parseInt(zoomMaxXpath.evaluate(areaNode));
+                    attr.minZoom = Integer.parseInt(zoomMinXpath.evaluate(areaNode));
+
+                    Double southLat = Double.parseDouble(southLatXpath.evaluate(areaNode));
+                    Double northLat = Double.parseDouble(northLatXpath.evaluate(areaNode));
+                    Double westLon = Double.parseDouble(westLonXpath.evaluate(areaNode));
+                    Double eastLon = Double.parseDouble(eastLonXpath.evaluate(areaNode));
+                    attr.min = new Coordinate(southLat, westLon);
+                    attr.max = new Coordinate(northLat, eastLon);
+
+                    attributions.add(attr);
+                }
+            }
+
+            return attributions;
+        } catch (SAXException e) {
+            System.err.println("Could not parse Bing aerials attribution metadata.");
+            e.printStackTrace();
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+        } catch (XPathExpressionException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    @Override
+    public int getMaxZoom() {
+        if(imageryZoomMax != null)
+            return imageryZoomMax;
+        else
+            return 22;
+    }
+
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+
+    @Override
+    public boolean requiresAttribution() {
+        return true;
+    }
+
+    @Override
+    public String getAttributionLinkURL() {
+        //return "http://bing.com/maps"
+        // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU
+        // (the requirement is that we have such a link at the bottom of the window)
+        return "http://go.microsoft.com/?linkid=9710837";
+    }
+
+    @Override
+    public Image getAttributionImage() {
+        try {
+            final InputStream imageResource = JMapViewer.class.getResourceAsStream("images/bing_maps.png");
+            if (imageResource != null) {
+                return ImageIO.read(imageResource);
+            } else {
+                // Some Linux distributions (like Debian) will remove Bing logo from sources, so get it at runtime
+                for (int i = 0; i < 5 && getAttribution() == null; i++) {
+                    // Makes sure attribution is loaded
+                }
+                if (brandLogoUri != null && !brandLogoUri.isEmpty()) {
+                    System.out.println("Reading Bing logo from "+brandLogoUri);
+                    return ImageIO.read(new URL(brandLogoUri));
+                }
+            }
+        } catch (IOException e) {
+            System.err.println("Error while retrieving Bing logo: "+e.getMessage());
+        }
+        return null;
+    }
+
+    @Override
+    public String getAttributionImageURL() {
+        return "http://opengeodata.org/microsoft-imagery-details";
+    }
+
+    @Override
+    public String getTermsOfUseText() {
+        return null;
+    }
+
+    @Override
+    public String getTermsOfUseURL() {
+        return "http://opengeodata.org/microsoft-imagery-details";
+    }
+
+    protected Callable<List<Attribution>> getAttributionLoaderCallable() {
+        return new Callable<List<Attribution>>() {
+
+            @Override
+            public List<Attribution> call() throws Exception {
+                int waitTimeSec = 1;
+                while (true) {
+                    try {
+                        InputSource xml = new InputSource(getAttributionUrl().openStream());
+                        List<Attribution> r = parseAttributionText(xml);
+                        System.out.println("Successfully loaded Bing attribution data.");
+                        return r;
+                    } catch (IOException ex) {
+                        System.err.println("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
+                        Thread.sleep(waitTimeSec * 1000L);
+                        waitTimeSec *= 2;
+                    }
+                }
+            }
+        };
+    }
+
+    protected List<Attribution> getAttribution() {
+        if (attributions == null) {
+            // see http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
+            synchronized (BingAerialTileSource.class) {
+                if (attributions == null) {
+                    attributions = Executors.newSingleThreadExecutor().submit(getAttributionLoaderCallable());
+                }
+            }
+        }
+        try {
+            return attributions.get(1000, TimeUnit.MILLISECONDS);
+        } catch (TimeoutException ex) {
+            System.err.println("Bing: attribution data is not yet loaded.");
+        } catch (ExecutionException ex) {
+            throw new RuntimeException(ex.getCause());
+        } catch (InterruptedException ign) {
+        }
+        return null;
+    }
+
+    @Override
+    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
+        try {
+            final List<Attribution> data = getAttribution();
+            if (data == null)
+                return "Error loading Bing attribution data";
+            StringBuilder a = new StringBuilder();
+            for (Attribution attr : data) {
+                if (zoom <= attr.maxZoom && zoom >= attr.minZoom) {
+                    if (topLeft.getLon() < attr.max.getLon() && botRight.getLon() > attr.min.getLon()
+                            && topLeft.getLat() > attr.min.getLat() && botRight.getLat() < attr.max.getLat()) {
+                        a.append(attr.attribution);
+                        a.append(" ");
+                    }
+                }
+            }
+            return a.toString();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return "Error loading Bing attribution data";
+    }
+
+    static String computeQuadTree(int zoom, int tilex, int tiley) {
+        StringBuilder k = new StringBuilder();
+        for (int i = zoom; i > 0; i--) {
+            char digit = 48;
+            int mask = 1 << (i - 1);
+            if ((tilex & mask) != 0) {
+                digit += 1;
+            }
+            if ((tiley & mask) != 0) {
+                digit += 2;
+            }
+            k.append(digit);
+        }
+        return k.toString();
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java
index 1edaf5d..ab83fe3 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java
@@ -1,78 +1,78 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.tilesources;
-
-/**
- * OSM Tile source.
- */
-public class OsmTileSource {
-
-    /**
-     * The default "Mapnik" OSM tile source.
-     */
-    public static class Mapnik extends AbstractOsmTileSource {
-
-        private static final String PATTERN = "https://%s.tile.openstreetmap.org";
-
-        private static final String[] SERVER = { "a", "b", "c" };
-
-        private int SERVER_NUM = 0;
-
-        /**
-         * Constructs a new {@code "Mapnik"} tile source.
-         */
-        public Mapnik() {
-            super("Mapnik", PATTERN, "MAPNIK");
-        }
-
-        @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 19;
-        }
-
-        public TileUpdate getTileUpdate() {
-            return TileUpdate.IfNoneMatch;
-        }
-    }
-
-    /**
-     * The "Cycle Map" OSM tile source.
-     */
-    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;
-
-        /**
-         * Constructs a new {@code CycleMap} tile source.
-         */
-        public CycleMap() {
-            super("Cyclemap", PATTERN, "opencyclemap");
-        }
-
-        @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 18;
-        }
-
-        public TileUpdate getTileUpdate() {
-            return TileUpdate.LastModified;
-        }
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+/**
+ * OSM Tile source.
+ */
+public class OsmTileSource {
+
+    /**
+     * The default "Mapnik" OSM tile source.
+     */
+    public static class Mapnik extends AbstractOsmTileSource {
+
+        private static final String PATTERN = "https://%s.tile.openstreetmap.org";
+
+        private static final String[] SERVER = { "a", "b", "c" };
+
+        private int SERVER_NUM = 0;
+
+        /**
+         * Constructs a new {@code "Mapnik"} tile source.
+         */
+        public Mapnik() {
+            super("Mapnik", PATTERN, "MAPNIK");
+        }
+
+        @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 19;
+        }
+
+        public TileUpdate getTileUpdate() {
+            return TileUpdate.IfNoneMatch;
+        }
+    }
+
+    /**
+     * The "Cycle Map" OSM tile source.
+     */
+    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;
+
+        /**
+         * Constructs a new {@code CycleMap} tile source.
+         */
+        public CycleMap() {
+            super("Cyclemap", PATTERN, "opencyclemap");
+        }
+
+        @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 18;
+        }
+
+        public TileUpdate getTileUpdate() {
+            return TileUpdate.LastModified;
+        }
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java
index be7128a..b12b98e 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java
@@ -1,158 +1,158 @@
-// License: BSD or GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.tilesources;
-
-import java.util.Random;
-
-import org.openstreetmap.gui.jmapviewer.OsmMercator;
-
-/*
- * This tilesource uses different to OsmMercator projection.
- *
- * Earth is assumed an ellipsoid in this projection, unlike
- * sphere in OsmMercator, so latitude calculation differs
- * a lot.
- *
- * The longitude calculation is the same as in OsmMercator,
- * we inherit it from AbstractTMSTileSource.
- *
- * TODO: correct getDistance() method.
- */
-
-public class ScanexTileSource extends TMSTileSource {
-    private static final String DEFAULT_URL = "http://maps.kosmosnimki.ru";
-    private static final int DEFAULT_MAXZOOM = 14;
-    private static String API_KEY = "4018C5A9AECAD8868ED5DEB2E41D09F7";
-
-    private enum ScanexLayer {
-        IRS("irs", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=BAC78D764F0443BD9AF93E7A998C9F5B"),
-        SPOT("spot", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=F51CE95441284AF6B2FC319B609C7DEC");
-
-        private String name;
-        private String uri;
-
-        ScanexLayer(String name, String uri) {
-            this.name = name;
-            this.uri = uri;
-        }
-        public String getName() {
-            return name;
-        }
-        public String getUri() {
-            return uri;
-        }
-    }
-
-    /* IRS by default */
-    private ScanexLayer Layer = ScanexLayer.IRS;
-
-    public ScanexTileSource(String name, String url, String id, int maxZoom) {
-        super(name, url, id, maxZoom);
-
-        for (ScanexLayer layer : ScanexLayer.values()) {
-            if (url.equalsIgnoreCase(layer.getName())) {
-                this.Layer = layer;
-                /*
-                 * Override baseUrl and maxZoom in base class.
-                 */
-                this.baseUrl = DEFAULT_URL;
-                if (maxZoom == 0)
-                    this.maxZoom = DEFAULT_MAXZOOM;
-                break;
-            }
-        }
-    }
-
-    @Override
-    public String getExtension() {
-        return("jpeg");
-    }
-
-    @Override
-    public String getTilePath(int zoom, int tilex, int tiley) {
-        int tmp = (int)Math.pow(2.0, zoom - 1);
-
-        tilex = tilex - tmp;
-        tiley = tmp - tiley - 1;
-
-        return this.Layer.getUri() + "&apikey=" + API_KEY + "&x=" + tilex + "&y=" + tiley + "&z=" + zoom;
-    }
-
-    public TileUpdate getTileUpdate() {
-        return TileUpdate.IfNoneMatch;
-    }
-
-
-    /*
-     * Latitude to Y and back calculations.
-     */
-    private static double RADIUS_E = 6378137;   /* radius of Earth at equator, m */
-    private static double EQUATOR = 40075016.68557849; /* equator length, m */
-    private static double E = 0.0818191908426;  /* eccentricity of Earth's ellipsoid */
-
-    @Override
-    public int LatToY(double lat, int zoom) {
-        return (int )(latToTileY(lat, zoom) * OsmMercator.TILE_SIZE);
-    }
-
-    @Override
-    public double YToLat(int y, int zoom) {
-        return tileYToLat((double )y / OsmMercator.TILE_SIZE, zoom);
-    }
-
-    @Override
-    public double latToTileY(double lat, int zoom) {
-        double tmp = Math.tan(Math.PI/4 * (1 + lat/90));
-        double pow = Math.pow(Math.tan(Math.PI/4 + Math.asin(E * Math.sin(Math.toRadians(lat)))/2), E);
-
-        return (EQUATOR/2 - (RADIUS_E * Math.log(tmp/pow))) * Math.pow(2.0, zoom) / EQUATOR;
-    }
-
-    @Override
-    public double tileYToLat(int y, int zoom) {
-        return tileYToLat((double )y, zoom);
-    }
-
-    /*
-     * To solve inverse formula latitude = f(y) we use
-     * Newton's method. We cache previous calculated latitude,
-     * because new one is usually close to the old one. In case
-     * if solution gets out of bounds, we reset to a new random
-     * value.
-     */
-    private double cached_lat = 0;
-    private double tileYToLat(double y, int zoom) {
-        double lat0, lat;
-
-        lat = cached_lat;
-        do {
-            lat0 = lat;
-            lat = lat - Math.toDegrees(NextTerm(Math.toRadians(lat), y, zoom));
-            if (lat > OsmMercator.MAX_LAT || lat < OsmMercator.MIN_LAT) {
-                Random r = new Random();
-                lat = OsmMercator.MIN_LAT +
-                  r.nextInt((int )(OsmMercator.MAX_LAT - OsmMercator.MIN_LAT));
-            }
-        } while ((Math.abs(lat0 - lat) > 0.000001));
-
-        cached_lat = lat;
-
-        return (lat);
-    }
-
-    /* Next term in Newton's polynomial */
-    private double NextTerm(double lat, double y, int zoom) {
-        double sinl=Math.sin(lat);
-        double cosl=Math.cos(lat);
-        double ec, f, df;
-
-        zoom = (int )Math.pow(2.0, zoom - 1);
-        ec = Math.exp((1 - y/zoom)*Math.PI);
-
-        f = (Math.tan(Math.PI/4+lat/2) -
-            ec * Math.pow(Math.tan(Math.PI/4 + Math.asin(E * sinl)/2), E));
-        df = 1/(1 - sinl) - ec * E * cosl/((1 - E * sinl) *
-            (Math.sqrt (1 - E * E * sinl * sinl)));
-
-        return (f/df);
-    }
-}
+// License: BSD or GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+import java.util.Random;
+
+import org.openstreetmap.gui.jmapviewer.OsmMercator;
+
+/*
+ * This tilesource uses different to OsmMercator projection.
+ *
+ * Earth is assumed an ellipsoid in this projection, unlike
+ * sphere in OsmMercator, so latitude calculation differs
+ * a lot.
+ *
+ * The longitude calculation is the same as in OsmMercator,
+ * we inherit it from AbstractTMSTileSource.
+ *
+ * TODO: correct getDistance() method.
+ */
+
+public class ScanexTileSource extends TMSTileSource {
+    private static final String DEFAULT_URL = "http://maps.kosmosnimki.ru";
+    private static final int DEFAULT_MAXZOOM = 14;
+    private static String API_KEY = "4018C5A9AECAD8868ED5DEB2E41D09F7";
+
+    private enum ScanexLayer {
+        IRS("irs", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=BAC78D764F0443BD9AF93E7A998C9F5B"),
+        SPOT("spot", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=F51CE95441284AF6B2FC319B609C7DEC");
+
+        private String name;
+        private String uri;
+
+        ScanexLayer(String name, String uri) {
+            this.name = name;
+            this.uri = uri;
+        }
+        public String getName() {
+            return name;
+        }
+        public String getUri() {
+            return uri;
+        }
+    }
+
+    /* IRS by default */
+    private ScanexLayer Layer = ScanexLayer.IRS;
+
+    public ScanexTileSource(String name, String url, String id, int maxZoom) {
+        super(name, url, id, maxZoom);
+
+        for (ScanexLayer layer : ScanexLayer.values()) {
+            if (url.equalsIgnoreCase(layer.getName())) {
+                this.Layer = layer;
+                /*
+                 * Override baseUrl and maxZoom in base class.
+                 */
+                this.baseUrl = DEFAULT_URL;
+                if (maxZoom == 0)
+                    this.maxZoom = DEFAULT_MAXZOOM;
+                break;
+            }
+        }
+    }
+
+    @Override
+    public String getExtension() {
+        return("jpeg");
+    }
+
+    @Override
+    public String getTilePath(int zoom, int tilex, int tiley) {
+        int tmp = (int)Math.pow(2.0, zoom - 1);
+
+        tilex = tilex - tmp;
+        tiley = tmp - tiley - 1;
+
+        return this.Layer.getUri() + "&apikey=" + API_KEY + "&x=" + tilex + "&y=" + tiley + "&z=" + zoom;
+    }
+
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+
+
+    /*
+     * Latitude to Y and back calculations.
+     */
+    private static double RADIUS_E = 6378137;   /* radius of Earth at equator, m */
+    private static double EQUATOR = 40075016.68557849; /* equator length, m */
+    private static double E = 0.0818191908426;  /* eccentricity of Earth's ellipsoid */
+
+    @Override
+    public int LatToY(double lat, int zoom) {
+        return (int )(latToTileY(lat, zoom) * OsmMercator.TILE_SIZE);
+    }
+
+    @Override
+    public double YToLat(int y, int zoom) {
+        return tileYToLat((double )y / OsmMercator.TILE_SIZE, zoom);
+    }
+
+    @Override
+    public double latToTileY(double lat, int zoom) {
+        double tmp = Math.tan(Math.PI/4 * (1 + lat/90));
+        double pow = Math.pow(Math.tan(Math.PI/4 + Math.asin(E * Math.sin(Math.toRadians(lat)))/2), E);
+
+        return (EQUATOR/2 - (RADIUS_E * Math.log(tmp/pow))) * Math.pow(2.0, zoom) / EQUATOR;
+    }
+
+    @Override
+    public double tileYToLat(int y, int zoom) {
+        return tileYToLat((double )y, zoom);
+    }
+
+    /*
+     * To solve inverse formula latitude = f(y) we use
+     * Newton's method. We cache previous calculated latitude,
+     * because new one is usually close to the old one. In case
+     * if solution gets out of bounds, we reset to a new random
+     * value.
+     */
+    private double cached_lat = 0;
+    private double tileYToLat(double y, int zoom) {
+        double lat0, lat;
+
+        lat = cached_lat;
+        do {
+            lat0 = lat;
+            lat = lat - Math.toDegrees(NextTerm(Math.toRadians(lat), y, zoom));
+            if (lat > OsmMercator.MAX_LAT || lat < OsmMercator.MIN_LAT) {
+                Random r = new Random();
+                lat = OsmMercator.MIN_LAT +
+                  r.nextInt((int )(OsmMercator.MAX_LAT - OsmMercator.MIN_LAT));
+            }
+        } while ((Math.abs(lat0 - lat) > 0.000001));
+
+        cached_lat = lat;
+
+        return (lat);
+    }
+
+    /* Next term in Newton's polynomial */
+    private double NextTerm(double lat, double y, int zoom) {
+        double sinl=Math.sin(lat);
+        double cosl=Math.cos(lat);
+        double ec, f, df;
+
+        zoom = (int )Math.pow(2.0, zoom - 1);
+        ec = Math.exp((1 - y/zoom)*Math.PI);
+
+        f = (Math.tan(Math.PI/4+lat/2) -
+            ec * Math.pow(Math.tan(Math.PI/4 + Math.asin(E * sinl)/2), E));
+        df = 1/(1 - sinl) - ec * E * cosl/((1 - E * sinl) *
+            (Math.sqrt (1 - E * E * sinl * sinl)));
+
+        return (f/df);
+    }
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java
index 23c792d..7f47c6a 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java
@@ -1,34 +1,34 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.tilesources;
-
-public class TMSTileSource extends AbstractTMSTileSource {
-
-    protected int maxZoom;
-    protected int minZoom = 0;
-
-    public TMSTileSource(String name, String url, String id, int maxZoom) {
-        super(name, url, id);
-        this.maxZoom = maxZoom;
-    }
-
-    public TMSTileSource(String name, String url, String id, int minZoom, int maxZoom) {
-        super(name, url, id);
-        this.minZoom = minZoom;
-        this.maxZoom = maxZoom;
-    }
-
-    @Override
-    public int getMinZoom() {
-        return (minZoom == 0) ? super.getMinZoom() : minZoom;
-    }
-
-    @Override
-    public int getMaxZoom() {
-        return (maxZoom == 0) ? super.getMaxZoom() : maxZoom;
-    }
-
-    @Override
-    public TileUpdate getTileUpdate() {
-        return TileUpdate.IfNoneMatch;
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+public class TMSTileSource extends AbstractTMSTileSource {
+
+    protected int maxZoom;
+    protected int minZoom = 0;
+
+    public TMSTileSource(String name, String url, String id, int maxZoom) {
+        super(name, url, id);
+        this.maxZoom = maxZoom;
+    }
+
+    public TMSTileSource(String name, String url, String id, int minZoom, int maxZoom) {
+        super(name, url, id);
+        this.minZoom = minZoom;
+        this.maxZoom = maxZoom;
+    }
+
+    @Override
+    public int getMinZoom() {
+        return (minZoom == 0) ? super.getMinZoom() : minZoom;
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return (maxZoom == 0) ? super.getMaxZoom() : maxZoom;
+    }
+
+    @Override
+    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
index a5d5c04..98417fb 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
@@ -1,87 +1,96 @@
-// License: GPL. For details, see Readme.txt file.
-package org.openstreetmap.gui.jmapviewer.tilesources;
-
-import java.util.Map;
-import java.util.HashMap;
-import java.util.Random;
-import java.util.regex.Pattern;
-import java.util.regex.Matcher;
-
-public class TemplatedTMSTileSource extends TMSTileSource {
-
-    private Random rand = null;
-    private String[] randomParts = null;
-    private Map<String, String> headers = new HashMap<>();
-
-    public static final String PATTERN_ZOOM    = "\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}";
-    public static final String PATTERN_X       = "\\{x\\}";
-    public static final String PATTERN_Y       = "\\{y\\}";
-    public static final String PATTERN_Y_YAHOO = "\\{!y\\}";
-    public static final String PATTERN_NEG_Y   = "\\{-y\\}";
-    public static final String PATTERN_SWITCH  = "\\{switch:([^}]+)\\}";
-    public static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
-
-    public static final String[] ALL_PATTERNS = {
-        PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y,
-        PATTERN_SWITCH
-    };
-
-    public TemplatedTMSTileSource(String name, String url, String id, int maxZoom) {
-        super(name, url, id, maxZoom);
-        handleTemplate();
-    }
-
-    public TemplatedTMSTileSource(String name, String url, String id, int minZoom, int maxZoom) {
-        super(name, url, id, minZoom, maxZoom);
-        handleTemplate();
-    }
-
-    private void handleTemplate() {
-        // Capturing group pattern on switch values
-        Matcher m = Pattern.compile(".*"+PATTERN_SWITCH+".*").matcher(baseUrl);
-        if (m.matches()) {
-            rand = new Random();
-            randomParts = m.group(1).split(",");
-        }
-        Pattern pattern = Pattern.compile(PATTERN_HEADER);
-        StringBuffer output = new StringBuffer();
-        Matcher matcher = pattern.matcher(baseUrl);
-        while (matcher.find()) {
-            headers.put(matcher.group(1),matcher.group(2));
-            matcher.appendReplacement(output, "");
-        }
-        matcher.appendTail(output);
-        baseUrl = output.toString();
-    }
-
-    public Map<String, String> getHeaders() {
-        return headers;
-    }
-
-    @Override
-    public String getTileUrl(int zoom, int tilex, int tiley) {
-        int finalZoom = zoom;
-        Matcher m = Pattern.compile(".*"+PATTERN_ZOOM+".*").matcher(this.baseUrl);
-        if (m.matches()) {
-            if(m.group(1) != null) {
-                finalZoom = Integer.valueOf(m.group(1))-zoom;
-            }
-            if(m.group(2) != null) {
-                String ofs = m.group(2);
-                if(ofs.startsWith("+"))
-                    ofs = ofs.substring(1);
-                finalZoom += Integer.valueOf(ofs);
-            }
-        }
-        String r = this.baseUrl
-            .replaceAll(PATTERN_ZOOM, Integer.toString(finalZoom))
-            .replaceAll(PATTERN_X, Integer.toString(tilex))
-            .replaceAll(PATTERN_Y, Integer.toString(tiley))
-            .replaceAll(PATTERN_Y_YAHOO, Integer.toString((int)Math.pow(2, zoom-1)-1-tiley))
-            .replaceAll(PATTERN_NEG_Y, Integer.toString((int)Math.pow(2, zoom)-1-tiley));
-        if (rand != null) {
-            r = r.replaceAll(PATTERN_SWITCH, randomParts[rand.nextInt(randomParts.length)]);
-        }
-        return r;
-    }
-}
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Random;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+public class TemplatedTMSTileSource extends TMSTileSource {
+
+    private Random rand = null;
+    private String[] randomParts = null;
+    private Map<String, String> headers = new HashMap<>();
+
+    public static final String COOKIE_HEADER   = "Cookie";
+    public static final String PATTERN_ZOOM    = "\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}";
+    public static final String PATTERN_X       = "\\{x\\}";
+    public static final String PATTERN_Y       = "\\{y\\}";
+    public static final String PATTERN_Y_YAHOO = "\\{!y\\}";
+    public static final String PATTERN_NEG_Y   = "\\{-y\\}";
+    public static final String PATTERN_SWITCH  = "\\{switch:([^}]+)\\}";
+    public static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
+
+    public static final String[] ALL_PATTERNS = {
+        PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y,
+        PATTERN_SWITCH
+    };
+
+    public TemplatedTMSTileSource(String name, String url, String id, int maxZoom) {
+        super(name, url, id, maxZoom);
+        handleTemplate();
+    }
+
+    public TemplatedTMSTileSource(String name, String url, String id, int minZoom, int maxZoom) {
+        super(name, url, id, minZoom, maxZoom);
+        handleTemplate();
+    }
+
+    public TemplatedTMSTileSource(String name, String url, String id, int minZoom, int maxZoom, String cookies) {
+        super(name, url, id, minZoom, maxZoom);
+        if (cookies != null) {
+            headers.put(COOKIE_HEADER, cookies);
+        }
+        handleTemplate();
+    }
+
+    private void handleTemplate() {
+        // Capturing group pattern on switch values
+        Matcher m = Pattern.compile(".*"+PATTERN_SWITCH+".*").matcher(baseUrl);
+        if (m.matches()) {
+            rand = new Random();
+            randomParts = m.group(1).split(",");
+        }
+        Pattern pattern = Pattern.compile(PATTERN_HEADER);
+        StringBuffer output = new StringBuffer();
+        Matcher matcher = pattern.matcher(baseUrl);
+        while (matcher.find()) {
+            headers.put(matcher.group(1),matcher.group(2));
+            matcher.appendReplacement(output, "");
+        }
+        matcher.appendTail(output);
+        baseUrl = output.toString();
+    }
+
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
+
+    @Override
+    public String getTileUrl(int zoom, int tilex, int tiley) {
+        int finalZoom = zoom;
+        Matcher m = Pattern.compile(".*"+PATTERN_ZOOM+".*").matcher(this.baseUrl);
+        if (m.matches()) {
+            if(m.group(1) != null) {
+                finalZoom = Integer.valueOf(m.group(1))-zoom;
+            }
+            if(m.group(2) != null) {
+                String ofs = m.group(2);
+                if(ofs.startsWith("+"))
+                    ofs = ofs.substring(1);
+                finalZoom += Integer.valueOf(ofs);
+            }
+        }
+        String r = this.baseUrl
+            .replaceAll(PATTERN_ZOOM, Integer.toString(finalZoom))
+            .replaceAll(PATTERN_X, Integer.toString(tilex))
+            .replaceAll(PATTERN_Y, Integer.toString(tiley))
+            .replaceAll(PATTERN_Y_YAHOO, Integer.toString((int)Math.pow(2, zoom-1)-1-tiley))
+            .replaceAll(PATTERN_NEG_Y, Integer.toString((int)Math.pow(2, zoom)-1-tiley));
+        if (rand != null) {
+            r = r.replaceAll(PATTERN_SWITCH, randomParts[rand.nextInt(randomParts.length)]);
+        }
+        return r;
+    }
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/jmapviewer.git



More information about the Pkg-grass-devel mailing list