diff --git a/build.xml b/build.xml
index 04febc6..936e8dd 100644
--- a/build.xml
+++ b/build.xml
@@ -198,22 +198,7 @@ Build-Date: ${build.tstamp}
             <arg value="${mapcss.dir}/MapCSSParser.jj"/>
         </exec>
     </target>
-    <target name="-jaxb_win" if="isWindows">
-        <property name="xjc" value="${java.home}\..\bin\xjc.exe" />
-    </target>
-    <target name="-jaxb_nix" unless="isWindows">
-        <property name="xjc" value="${java.home}/../bin/xjc" />
-    </target>
-    <target name="jaxb" depends="init, -jaxb_win, -jaxb_nix" unless="jaxb.notRequired">
-        <exec executable="${xjc}" failonerror="true">
-            <arg value="-d"/>
-            <arg value="${src.dir}"/>
-            <arg value="-encoding"/>
-            <arg value="UTF-8"/>
-            <arg value="data_nodist/wms-cache.xsd"/>
-        </exec>
-    </target>
-    <target name="compile" depends="init,javacc,jaxb">
+    <target name="compile" depends="init,javacc">
         <!-- COTS -->
         <javac srcdir="${src.dir}" includes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**" nowarn="on" encoding="iso-8859-1"
             destdir="build" target="1.7" source="1.7" debug="on" includeAntRuntime="false" createMissingPackageInfoClass="false">
diff --git a/data_nodist/wms-cache.xsd b/data_nodist/wms-cache.xsd
deleted file mode 100644
index c6d50b8..0000000
--- a/data_nodist/wms-cache.xsd
+++ /dev/null
@@ -1,55 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://josm.openstreetmap.de/wms-cache-1.0"
-	xmlns:tns="http://josm.openstreetmap.de/wms-cache-1.0" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
-	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd"
-	elementFormDefault="qualified" jaxb:version="2.0">
-	
-	<annotation>
-		<appinfo>
-			<jaxb:schemaBindings>
-				<jaxb:package name="org.openstreetmap.josm.data.imagery.types">
-				</jaxb:package>
-				<jaxb:nameXmlTransform>
-					<jaxb:typeName suffix="Type" />
-					<jaxb:elementName suffix="Type" />
-				</jaxb:nameXmlTransform>
-			</jaxb:schemaBindings>
-			<jaxb:globalBindings>
-				<jaxb:javaType name="java.util.Calendar" xmlType="date"
-					parseMethod="javax.xml.bind.DatatypeConverter.parseDate"
-					printMethod="org.openstreetmap.josm.data.imagery.WmsCache.printDate" />
-			</jaxb:globalBindings>
-		</appinfo>
-	</annotation>
-
-	<element name="wms-cache">
-		<complexType>
-			<sequence>
-				<element name="projection" type="tns:projection" minOccurs="0"
-					maxOccurs="unbounded" />
-			</sequence>
-			<attribute name="tileSize" type="int" use="required" />
-			<attribute name="totalFileSize" type="int" use="required"/>
-		</complexType>
-	</element>
-	
-	<complexType name="projection">
-		<sequence>
-			<element name="entry" type="tns:entry" minOccurs="0" maxOccurs="unbounded"/>
-		</sequence>
-		<attribute name="name" type="string"/>
-		<attribute name="cache-directory" type="string"/>
-	</complexType>
-
-	<complexType name="entry">
-		<sequence>
-			<element name="pixelPerDegree" type="double" />
-			<element name="east" type="double" />
-			<element name="north" type="double" />
-			<element name="lastUsed" type="date" />
-			<element name="lastModified" type="date" />
-			<element name="filename" type="string" />
-		</sequence>
-	</complexType>
-</schema>
diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java
new file mode 100644
index 0000000..6c7e01a
--- /dev/null
+++ b/src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java
@@ -0,0 +1,18 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+import java.util.Map;
+
+/**
+ * Interface for template tile sources, @see TemplatedTMSTileSource
+ *
+ * @author Wiktor Niesiobędzki
+ * @since TODO
+ */
+public interface TemplatedTileSource extends TileSource {
+    /**
+     *
+     * @return headers to be sent with http requests
+     */
+    public Map<String, String> getHeaders();
+}
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
index 9c2518f..29d82db 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
@@ -8,7 +8,16 @@ import java.util.Map;
 import java.util.Map.Entry;
 
 import org.openstreetmap.gui.jmapviewer.OsmMercator;
-
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
+
+/**
+ * Class generalizing all tile based tile sources
+ *
+ * @author Wiktor Niesiobędzki
+ *
+ */
 public abstract class AbstractTMSTileSource extends AbstractTileSource {
 
     protected String name;
@@ -19,6 +28,11 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
     protected int tileSize;
     protected OsmMercator osmMercator;
 
+    /**
+     * Creates an instance based on TileSource information
+     *
+     * @param info description of the Tile Source
+     */
     public AbstractTMSTileSource(TileSourceInfo info) {
         this.name = info.getName();
         this.baseUrl = info.getUrl();
@@ -32,6 +46,13 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
         osmMercator = new OsmMercator(this.tileSize);
     }
 
+    /**
+     * @return default tile size to use, when not set in Imagery Preferences
+     */
+    protected int getDefaultTileSize() {
+        return OsmMercator.DEFAUL_TILE_SIZE;
+    }
+
     @Override
     public String getName() {
         return name;
@@ -52,17 +73,27 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
         return 0;
     }
 
+    /**
+     * @return image extension, used for URL creation
+     */
     public String getExtension() {
         return "png";
     }
 
     /**
+     * @param zoom level of the tile
+     * @param tilex tile number in x axis
+     * @param tiley tile number in y axis
+     * @return String containg path part of URL of the tile
      * @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();
     }
 
+    /**
+     * @return Base part of the URL of the tile source
+     */
     public String getBaseUrl() {
         return this.baseUrl;
     }
@@ -87,6 +118,9 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
      */
     @Override
     public int getTileSize() {
+        if (tileSize <= 0) {
+            return getDefaultTileSize();
+        };
         return tileSize;
     }
 
@@ -152,6 +186,26 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
         return super.isNoTileAtZoom(headers, statusCode, content);
     }
 
+    /**
+     * Converts imagery info to any of TMS supported TileSource
+     * @param info
+     * @return TileSource for specified info
+     * @throws IllegalArgumentException
+     */
+    public static TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
+        if (info.getImageryType() == ImageryType.TMS) {
+            TemplatedTMSTileSource.checkUrl(info.getUrl());
+            TMSTileSource t = new TemplatedTMSTileSource(info);
+            info.setAttribution(t);
+            return t;
+        } else if (info.getImageryType() == ImageryType.BING)
+            return new CachedAttributionBingAerialTileSource(info);
+        else if (info.getImageryType() == ImageryType.SCANEX) {
+            return new ScanexTileSource(info);
+        }
+        return null;
+    }
+
     @Override
     public Map<String, String> getMetadata(Map<String, List<String>> headers) {
         Map<String, String> ret = new HashMap<>();
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java
new file mode 100644
index 0000000..42f9cf4
--- /dev/null
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java
@@ -0,0 +1,73 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URL;
+import java.util.List;
+import java.util.Scanner;
+import java.util.concurrent.Callable;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.io.CacheCustomContent;
+import org.openstreetmap.josm.io.UTFInputStreamReader;
+import org.openstreetmap.josm.tools.Utils;
+import org.xml.sax.InputSource;
+
+/**
+ * Bing TileSource with cached attribution
+ *
+ * @author Wiktor Niesiobędzki
+ * @since TODO
+ *
+ */
+public class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
+    /**
+     * Creates tile source
+     * @param info ImageryInfo description of this tile source
+     */
+    public CachedAttributionBingAerialTileSource(ImageryInfo info) {
+        super(info);
+    }
+
+    class BingAttributionData extends CacheCustomContent<IOException> {
+
+        public BingAttributionData() {
+            super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
+        }
+
+        @Override
+        protected byte[] updateData() throws IOException {
+            URL u = getAttributionUrl();
+            try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) {
+                String r = scanner.useDelimiter("\\A").next();
+                Main.info("Successfully loaded Bing attribution data.");
+                return r.getBytes("UTF-8");
+            }
+        }
+    }
+
+    @Override
+    protected Callable<List<Attribution>> getAttributionLoaderCallable() {
+        return new Callable<List<Attribution>>() {
+
+            @Override
+            public List<Attribution> call() throws Exception {
+                BingAttributionData attributionLoader = new BingAttributionData();
+                int waitTimeSec = 1;
+                while (true) {
+                    try {
+                        String xml = attributionLoader.updateIfRequiredString();
+                        return parseAttributionText(new InputSource(new StringReader((xml))));
+                    } catch (IOException ex) {
+                        Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
+                        Thread.sleep(waitTimeSec * 1000L);
+                        waitTimeSec *= 2;
+                    }
+                }
+            }
+        };
+    }
+}
+
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
index 4b43ff0..1e18c48 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
@@ -1,33 +1,63 @@
 // License: GPL. For details, see Readme.txt file.
 package org.openstreetmap.gui.jmapviewer.tilesources;
 
+import static org.openstreetmap.josm.tools.I18n.tr;
+
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Random;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-public class TemplatedTMSTileSource extends TMSTileSource {
+import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * Handles templated TMS Tile Source. Templated means, that some patterns within
+ * URL gets substituted.
+ *
+ * Supported parameters
+ * {zoom} - substituted with zoom level
+ * {z} - as above
+ * {NUMBER-zoom} - substituted with result of equation "NUMBER - zoom",
+ *                  eg. {20-zoom} for zoom level 15 will result in 5 in this place
+ * {zoom+number} - substituted with result of equation "zoom + number",
+ *                 eg. {zoom+5} for zoom level 15 will result in 20.
+ * {x} - substituted with X tile number
+ * {y} - substituted with Y tile number
+ * {!y} - substituted with Yahoo Y tile number
+ * {-y} - substituted with reversed Y tile number
+ * {switch:VAL_A,VAL_B,VAL_C,...} - substituted with one of VAL_A, VAL_B, VAL_C. Usually
+ *                                  used to specify many tile servers
+ * {header:(HEADER_NAME,HEADER_VALUE)} - sets the headers to be sent to tile server
+ */
+
+public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTileSource {
 
     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\\(([^,]+),([^}]+)\\)\\}";
+    private static final String COOKIE_HEADER   = "Cookie";
+    private static final String PATTERN_ZOOM    = "\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}";
+    private static final String PATTERN_X       = "\\{x\\}";
+    private static final String PATTERN_Y       = "\\{y\\}";
+    private static final String PATTERN_Y_YAHOO = "\\{!y\\}";
+    private static final String PATTERN_NEG_Y   = "\\{-y\\}";
+    private static final String PATTERN_SWITCH  = "\\{switch:([^}]+)\\}";
+    private static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
 
-    public static final String[] ALL_PATTERNS = {
+    private static final String[] ALL_PATTERNS = {
         PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y,
         PATTERN_SWITCH
     };
 
-    public TemplatedTMSTileSource(TileSourceInfo info) {
+    /**
+     * Creates Templated TMS Tile Source based on ImageryInfo
+     * @param info
+     */
+    public TemplatedTMSTileSource(ImageryInfo info) {
         super(info);
         if (info.getCookies() != null) {
             headers.put(COOKIE_HEADER, info.getCookies());
@@ -83,4 +113,26 @@ public class TemplatedTMSTileSource extends TMSTileSource {
         }
         return r;
     }
+
+    /**
+     * Checks if url is acceptable by this Tile Source
+     * @param url
+     */
+    public static void checkUrl(String url) {
+        CheckParameterUtil.ensureParameterNotNull(url, "url");
+        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
+        while (m.find()) {
+            boolean isSupportedPattern = false;
+            for (String pattern : ALL_PATTERNS) {
+                if (m.group().matches(pattern)) {
+                    isSupportedPattern = true;
+                    break;
+                }
+            }
+            if (!isSupportedPattern) {
+                throw new IllegalArgumentException(
+                        tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
+            }
+        }
+    }
 }
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java
index 639a9d0..2aa48dd 100644
--- a/src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java
+++ b/src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java
@@ -3,8 +3,6 @@ package org.openstreetmap.gui.jmapviewer.tilesources;
 
 import java.util.Map;
 
-import org.openstreetmap.gui.jmapviewer.OsmMercator;
-
 /**
  * Data class that keeps basic information about a tile source.
  *
@@ -32,7 +30,7 @@ public class TileSourceInfo {
     protected String cookies;
 
     /** tile size of the displayed tiles */
-    private int tileSize = OsmMercator.DEFAUL_TILE_SIZE;
+    private int tileSize = -1; // use default
 
     /** mapping <header key, metadata key> */
     protected Map<String, String> metadataHeaders;
@@ -115,7 +113,7 @@ public class TileSourceInfo {
 
     /**
      * Request tile size of this tile source
-     * @return tile size provided by this tile source
+     * @return tile size provided by this tile source, or -1 when default value should be used
      */
     public int getTileSize() {
         return tileSize;
@@ -126,7 +124,7 @@ public class TileSourceInfo {
      * @param tileSize
      */
     public void setTileSize(int tileSize) {
-        if (tileSize <= 0) {
+        if (tileSize == 0 || tileSize < -1) {
             throw new AssertionError("Invalid tile size: " + tileSize);
         }
         this.tileSize = tileSize;
diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
index f494675..d28251c 100644
--- a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
+++ b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
@@ -189,7 +189,7 @@ public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements
                 return;
             }
             // object not in cache, so submit work to separate thread
-            getDownloadExecutor().execute(this);
+            downloadJobExecutor.execute(this);
         }
     }
 
@@ -226,13 +226,6 @@ public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements
         return getUrl().getHost();
     }
 
-    /**
-     * this needs to be non-static, so it can be overridden by subclasses
-     */
-    protected ThreadPoolExecutor getDownloadExecutor() {
-        return downloadJobExecutor;
-    }
-
     public void run() {
         final Thread currentThread = Thread.currentThread();
         final String oldName = currentThread.getName();
@@ -466,15 +459,12 @@ public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements
      * cancels all outstanding tasks in the queue.
      */
     public void cancelOutstandingTasks() {
-        ThreadPoolExecutor downloadExecutor = getDownloadExecutor();
-        for(Runnable r: downloadExecutor.getQueue()) {
-            if (downloadExecutor.remove(r)) {
-                if (r instanceof JCSCachedTileLoaderJob) {
+        for(Runnable r: downloadJobExecutor.getQueue()) {
+            if (downloadJobExecutor.remove(r) && r instanceof JCSCachedTileLoaderJob) {
                 ((JCSCachedTileLoaderJob<?, ?>) r).handleJobCancellation();
             }
         }
     }
-    }
 
     /**
      * Sets a job, that will be run, when job will finish execution
diff --git a/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java b/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java
new file mode 100644
index 0000000..24c04da
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java
@@ -0,0 +1,72 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.data.preferences.StringProperty;
+
+/**
+ * TileLoaderFactory creating JCS cached TileLoaders
+ *
+ * @author Wiktor Niesiobędzki
+ * @since TODO
+ *
+ */
+public abstract class CachedTileLoaderFactory implements TileLoaderFactory {
+    /**
+     * Keeps the cache directory where
+     */
+    public static final StringProperty PROP_TILECACHE_DIR = getTileCacheDir();
+    private String cacheName;
+
+    /**
+     * @param cacheName name of the cache region, that the created loader will use
+     */
+    public CachedTileLoaderFactory(String cacheName) {
+        this.cacheName = cacheName;
+    }
+
+    private static StringProperty getTileCacheDir() {
+        String defPath = null;
+        try {
+            defPath = new File(Main.pref.getCacheDirectory(), "tiles").getAbsolutePath();
+        } catch (SecurityException e) {
+            Main.warn(e);
+        }
+        return new StringProperty("imagery.generic.loader.cachedir", defPath);
+    }
+
+    @Override
+    public TileLoader makeTileLoader(TileLoaderListener listener) {
+        return makeTileLoader(listener, null);
+    }
+
+    @Override
+    public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
+        Map<String, String> headers = new HashMap<>();
+        headers.put("User-Agent", Version.getInstance().getFullAgentString());
+        headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
+        if (inputHeaders != null)
+            headers.putAll(inputHeaders);
+
+        try {
+            return getLoader(listener, cacheName,
+                    Main.pref.getInteger("socket.timeout.connect",15) * 1000,
+                    Main.pref.getInteger("socket.timeout.read", 30) * 1000,
+                    headers,
+                    PROP_TILECACHE_DIR.get());
+        } catch (IOException e) {
+            Main.warn(e);
+        }
+        return null;
+    }
+
+    protected abstract TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout, int readTimeout, Map<String, String> headers, String cacheDir) throws IOException;
+}
diff --git a/src/org/openstreetmap/josm/data/imagery/GeorefImage.java b/src/org/openstreetmap/josm/data/imagery/GeorefImage.java
deleted file mode 100644
index 228ece2..0000000
--- a/src/org/openstreetmap/josm/data/imagery/GeorefImage.java
+++ /dev/null
@@ -1,256 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.data.imagery;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.Color;
-import java.awt.Font;
-import java.awt.Graphics;
-import java.awt.Image;
-import java.awt.Transparency;
-import java.awt.image.BufferedImage;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-import java.lang.ref.SoftReference;
-
-import javax.imageio.ImageIO;
-
-import org.openstreetmap.josm.data.coor.EastNorth;
-import org.openstreetmap.josm.gui.NavigatableComponent;
-import org.openstreetmap.josm.gui.layer.ImageryLayer;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
-import org.openstreetmap.josm.tools.ImageProvider;
-
-public class GeorefImage implements Serializable {
-    private static final long serialVersionUID = 1L;
-
-    public enum State { IMAGE, NOT_IN_CACHE, FAILED, PARTLY_IN_CACHE}
-
-    private WMSLayer layer;
-    private State state;
-
-    private BufferedImage image;
-    private SoftReference<BufferedImage> reImg;
-    private int xIndex;
-    private int yIndex;
-
-    private static final Color transparentColor = new Color(0,0,0,0);
-    private Color fadeColor = transparentColor;
-
-    public EastNorth getMin() {
-        return layer.getEastNorth(xIndex, yIndex);
-    }
-
-    public EastNorth getMax() {
-        return layer.getEastNorth(xIndex+1, yIndex+1);
-    }
-
-    public GeorefImage(WMSLayer layer) {
-        this.layer = layer;
-    }
-
-    public void changePosition(int xIndex, int yIndex) {
-        if (!equalPosition(xIndex, yIndex)) {
-            this.xIndex = xIndex;
-            this.yIndex = yIndex;
-            this.image = null;
-            flushResizedCachedInstance();
-        }
-    }
-
-    public boolean equalPosition(int xIndex, int yIndex) {
-        return this.xIndex == xIndex && this.yIndex == yIndex;
-    }
-
-    /**
-     * Resets this image to initial state and release all resources being used.
-     * @since 7132
-     */
-    public void resetImage() {
-        if (image != null) {
-            image.flush();
-        }
-        changeImage(null, null, null);
-    }
-
-    public void changeImage(State state, BufferedImage image, String errorMsg) {
-        flushResizedCachedInstance();
-        this.image = image;
-        this.state = state;
-        if (state == null)
-            return;
-        switch (state) {
-        case FAILED:
-            BufferedImage imgFailed = createImage();
-            layer.drawErrorTile(imgFailed, errorMsg);
-            this.image = imgFailed;
-            break;
-        case NOT_IN_CACHE:
-            BufferedImage img = createImage();
-            Graphics g = img.getGraphics();
-            g.setColor(Color.GRAY);
-            g.fillRect(0, 0, img.getWidth(), img.getHeight());
-            Font font = g.getFont();
-            Font tempFont = font.deriveFont(Font.PLAIN).deriveFont(36.0f);
-            g.setFont(tempFont);
-            g.setColor(Color.BLACK);
-            String text = tr("Not in cache");
-            g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, img.getHeight()/2);
-            g.setFont(font);
-            this.image = img;
-            break;
-        default:
-            if (this.image != null) {
-                this.image = layer.sharpenImage(this.image);
-            }
-            break;
-        }
-    }
-
-    private BufferedImage createImage() {
-        return new BufferedImage(layer.getImageSize(), layer.getImageSize(), BufferedImage.TYPE_INT_RGB);
-    }
-
-    public boolean paint(Graphics g, NavigatableComponent nc, int xIndex, int yIndex, int leftEdge, int bottomEdge) {
-        if (getImage() == null)
-            return false;
-
-        if(!(this.xIndex == xIndex && this.yIndex == yIndex))
-            return false;
-
-        int left = layer.getImageX(xIndex);
-        int bottom = layer.getImageY(yIndex);
-        int width = layer.getImageWidth(xIndex);
-        int height = layer.getImageHeight(yIndex);
-
-        int x = left - leftEdge;
-        int y = nc.getHeight() - (bottom - bottomEdge) - height;
-
-        // This happens if you zoom outside the world
-        if(width == 0 || height == 0)
-            return false;
-
-        // TODO: implement per-layer fade color
-        Color newFadeColor;
-        if (ImageryLayer.PROP_FADE_AMOUNT.get() == 0) {
-            newFadeColor = transparentColor;
-        } else {
-            newFadeColor = ImageryLayer.getFadeColorWithAlpha();
-        }
-
-        BufferedImage img = reImg == null?null:reImg.get();
-        if(img != null && img.getWidth() == width && img.getHeight() == height && fadeColor.equals(newFadeColor)) {
-            g.drawImage(img, x, y, null);
-            return true;
-        }
-
-        fadeColor = newFadeColor;
-
-        boolean alphaChannel = WMSLayer.PROP_ALPHA_CHANNEL.get() && getImage().getTransparency() != Transparency.OPAQUE;
-
-        try {
-            if(img != null) {
-                img.flush();
-            }
-            long freeMem = Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory();
-            // Notice that this value can get negative due to integer overflows
-
-            int multipl = alphaChannel ? 4 : 3;
-            // This happens when requesting images while zoomed out and then zooming in
-            // Storing images this large in memory will certainly hang up JOSM. Luckily
-            // traditional rendering is as fast at these zoom levels, so it's no loss.
-            // Also prevent caching if we're out of memory soon
-            if(width > 2000 || height > 2000 || width*height*multipl > freeMem) {
-                fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);
-            } else {
-                // We haven't got a saved resized copy, so resize and cache it
-                img = new BufferedImage(width, height, alphaChannel?BufferedImage.TYPE_INT_ARGB:BufferedImage.TYPE_3BYTE_BGR);
-                img.getGraphics().drawImage(getImage(),
-                        0, 0, width, height, // dest
-                        0, 0, getImage().getWidth(null), getImage().getHeight(null), // src
-                        null);
-                if (!alphaChannel) {
-                    drawFadeRect(img.getGraphics(), 0, 0, width, height);
-                }
-                img.getGraphics().dispose();
-                g.drawImage(img, x, y, null);
-                reImg = new SoftReference<>(img);
-            }
-        } catch(Exception e) {
-            fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);
-        }
-        return true;
-    }
-
-    private void fallbackDraw(Graphics g, Image img, int x, int y, int width, int height, boolean alphaChannel) {
-        flushResizedCachedInstance();
-        g.drawImage(
-                img, x, y, x + width, y + height,
-                0, 0, img.getWidth(null), img.getHeight(null),
-                null);
-        if (!alphaChannel) { //FIXME: fading for layers with alpha channel currently is not supported
-            drawFadeRect(g, x, y, width, height);
-        }
-    }
-
-    private void drawFadeRect(Graphics g, int x, int y, int width, int height) {
-        if (fadeColor != transparentColor) {
-            g.setColor(fadeColor);
-            g.fillRect(x, y, width, height);
-        }
-    }
-
-    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
-        state = (State) in.readObject();
-        boolean hasImage = in.readBoolean();
-        if (hasImage) {
-            image = ImageProvider.read(ImageIO.createImageInputStream(in), true, WMSLayer.PROP_ALPHA_CHANNEL.get());
-        } else {
-            in.readObject(); // read null from input stream
-            image = null;
-        }
-    }
-
-    private void writeObject(ObjectOutputStream out) throws IOException {
-        out.writeObject(state);
-        if(getImage() == null) {
-            out.writeBoolean(false);
-            out.writeObject(null);
-        } else {
-            out.writeBoolean(true);
-            ImageIO.write(getImage(), "png", ImageIO.createImageOutputStream(out));
-        }
-    }
-
-    public void flushResizedCachedInstance() {
-        if (reImg != null) {
-            BufferedImage img = reImg.get();
-            if (img != null) {
-                img.flush();
-            }
-        }
-        reImg = null;
-    }
-
-    public BufferedImage getImage() {
-        return image;
-    }
-
-    public State getState() {
-        return state;
-    }
-
-    public int getXIndex() {
-        return xIndex;
-    }
-
-    public int getYIndex() {
-        return yIndex;
-    }
-
-    public void setLayer(WMSLayer layer) {
-        this.layer = layer;
-    }
-}
diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java
index b8d92c1..e63ae0f 100644
--- a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java
+++ b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java
@@ -28,16 +28,18 @@ import org.openstreetmap.josm.data.preferences.IntegerProperty;
  */
 public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCache {
 
-    private ICacheAccess<String, BufferedImageCacheEntry> cache;
-    private int connectTimeout;
-    private int readTimeout;
-    private Map<String, String> headers;
-    private TileLoaderListener listener;
+    protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
+    protected final int connectTimeout;
+    protected final int readTimeout;
+    protected final Map<String, String> headers;
+    protected final TileLoaderListener listener;
     private static final String PREFERENCE_PREFIX   = "imagery.tms.cache.";
     /**
      * how many object on disk should be stored for TMS region. Average tile size is about 20kb
+     *
+     * 25000 is around 500MB under this assumptions
      */
-    public static final IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000); // 25000 is around 500MB under this assumptions
+    public static final IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000);
 
     /**
      * overrides the THREAD_LIMIT in superclass, as we want to have separate limit and pool for TMS
@@ -54,18 +56,7 @@ public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa
      * separate from JCS thread pool for TMS loader, so we can have different thread pools for default JCS
      * and for TMS imagery
      */
-    private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getThreadPoolExecutor();
-
-    private static ThreadPoolExecutor getThreadPoolExecutor() {
-        return new ThreadPoolExecutor(
-                THREAD_LIMIT.get().intValue(), // keep the thread number constant
-                THREAD_LIMIT.get().intValue(), // do not this number of threads
-                30, // keepalive for thread
-                TimeUnit.SECONDS,
-                new HostLimitQueue(HOST_LIMIT.get().intValue()),
-                JCSCachedTileLoaderJob.getNamedThreadFactory("TMS downloader")
-                );
-    }
+    private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getNewThreadPoolExecutor("TMS downloader");
 
     private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER;
 
@@ -90,10 +81,34 @@ public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa
         this.listener = listener;
     }
 
+    /**
+     * @param name name of the threads
+     * @param workers number of worker thread to keep
+     * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue
+     */
+    public static ThreadPoolExecutor getNewThreadPoolExecutor(String name, int workers) {
+        return new ThreadPoolExecutor(
+                workers, // keep the thread number constant
+                workers, // do not this number of threads
+                30, // keepalive for thread
+                TimeUnit.SECONDS,
+                new HostLimitQueue(HOST_LIMIT.get().intValue()),
+                JCSCachedTileLoaderJob.getNamedThreadFactory(name)
+                );
+    }
+
+    /**
+     * @param name name of threads
+     * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue, with default number of threads
+     */
+    public static ThreadPoolExecutor getNewThreadPoolExecutor(String name) {
+        return getNewThreadPoolExecutor(name, THREAD_LIMIT.get().intValue());
+    }
+
     @Override
     public TileJob createTileLoaderJob(Tile tile) {
         return new TMSCachedTileLoaderJob(listener, tile, cache,
-                connectTimeout, readTimeout, headers, downloadExecutor);
+                connectTimeout, readTimeout, headers, getDownloadExecutor());
     }
 
     @Override
@@ -139,4 +154,23 @@ public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa
             }
         }
     }
+
+    /**
+     * Sets the download executor that will be used to download tiles instead of default one.
+     * You can use {@link #getNewThreadPoolExecutor} to create a new download executor with separate
+     * queue from default.
+     *
+     * @param downloadExecutor
+     */
+    public void setDownloadExecutor(ThreadPoolExecutor downloadExecutor) {
+        this.downloadExecutor = downloadExecutor;
+    }
+
+    /**
+     * @return download executor that is used by this factory
+     */
+    public ThreadPoolExecutor getDownloadExecutor() {
+        return downloadExecutor;
+    }
+
 }
diff --git a/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java b/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
new file mode 100644
index 0000000..3d13b31
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
@@ -0,0 +1,183 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * Tile Source handling WMS providers
+ *
+ * @author Wiktor Niesiobędzki
+ * @since TODO
+ *
+ */
+public class TemplatedWMSTileSource extends TMSTileSource implements TemplatedTileSource {
+    private Map<String, String> headers = new HashMap<>();
+    private ImageryInfo info;
+    private static final String COOKIE_HEADER   = "Cookie";
+    private static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
+    private static final String PATTERN_PROJ    = "\\{proj(\\([^})]+\\))?\\}";
+    private static final String PATTERN_BBOX    = "\\{bbox\\}";
+    private static final String PATTERN_W       = "\\{w\\}";
+    private static final String PATTERN_S       = "\\{s\\}";
+    private static final String PATTERN_E       = "\\{e\\}";
+    private static final String PATTERN_N       = "\\{n\\}";
+    private static final String PATTERN_WIDTH   = "\\{width\\}";
+    private static final String PATTERN_HEIGHT  = "\\{height\\}";
+
+
+    private static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
+
+    private static final String[] ALL_PATTERNS = {
+        PATTERN_HEADER, PATTERN_PROJ, PATTERN_BBOX, PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N, PATTERN_WIDTH, PATTERN_HEIGHT
+    };
+
+    /**
+     * Creates a tile source based on imagery info
+     * @param info
+     */
+    public TemplatedWMSTileSource(ImageryInfo info) {
+        super(info);
+        this.info = info;
+        if (info.getCookies() != null) {
+            headers.put(COOKIE_HEADER, info.getCookies());
+        }
+
+        handleTemplate();
+    }
+
+    private void handleTemplate() {
+        // Capturing group pattern on switch values
+        Pattern pattern = Pattern.compile(PATTERN_HEADER);
+        StringBuffer output = new StringBuffer();
+        Matcher matcher = pattern.matcher(this.baseUrl);
+        while (matcher.find()) {
+            headers.put(matcher.group(1),matcher.group(2));
+            matcher.appendReplacement(output, "");
+        }
+        matcher.appendTail(output);
+        this.baseUrl = output.toString();
+    }
+
+    @Override
+    protected int getDefaultTileSize() {
+        return WMSLayer.PROP_IMAGE_SIZE.get();
+    }
+
+    @Override
+    public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
+        Projection myProj = Main.getProjection();
+        String myProjCode = Main.getProjection().toCode();
+
+        EastNorth sw = myProj.latlon2eastNorth(new LatLon(
+                tileYToLat(tiley+1, zoom),
+                tileXToLon(tilex, zoom)
+                ));
+        EastNorth ne = myProj.latlon2eastNorth(new LatLon(
+                tileYToLat(tiley, zoom),
+                tileXToLon(tilex+1, zoom)
+                ));
+
+        double w = sw.getX();
+        double s = sw.getY();
+
+        double e = ne.getX();
+        double n = ne.getY();
+
+        if (!info.getServerProjections().contains(myProjCode) && "EPSG:3857".equals(Main.getProjection().toCode())) {
+            LatLon swll = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));
+            LatLon nell = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));
+            myProjCode = "EPSG:4326";
+            s = swll.lat();
+            w = swll.lon();
+            n = nell.lat();
+            e = nell.lon();
+        }
+        if ("EPSG:4326".equals(myProjCode) && !info.getServerProjections().contains(myProjCode) && info.getServerProjections().contains("CRS:84")) {
+            myProjCode = "CRS:84";
+        }
+
+        // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
+        //
+        // Background:
+        //
+        // bbox=x_min,y_min,x_max,y_max
+        //
+        //      SRS=... is WMS 1.1.1
+        //      CRS=... is WMS 1.3.0
+        //
+        // The difference:
+        //      For SRS x is east-west and y is north-south
+        //      For CRS x and y are as specified by the EPSG
+        //          E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
+        //          For most other EPSG code there seems to be no difference.
+        // [1] https://www.epsg-registry.org/report.htm?type=selection&entity=urn:ogc:def:crs:EPSG::4326&reportDetail=short&style=urn:uuid:report-style:default-with-code&style_name=OGP%20Default%20With%20Code&title=EPSG:4326
+        boolean switchLatLon = false;
+        if (baseUrl.toLowerCase().contains("crs=epsg:4326")) {
+            switchLatLon = true;
+        } else if (baseUrl.toLowerCase().contains("crs=") && "EPSG:4326".equals(myProjCode)) {
+            switchLatLon = true;
+        }
+        String bbox;
+        if (switchLatLon) {
+            bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e));
+        } else {
+            bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n));
+        }
+        return baseUrl.
+                replaceAll(PATTERN_PROJ,    myProjCode)
+                .replaceAll(PATTERN_BBOX,   bbox)
+                .replaceAll(PATTERN_W,      latLonFormat.format(w))
+                .replaceAll(PATTERN_S,      latLonFormat.format(s))
+                .replaceAll(PATTERN_E,      latLonFormat.format(e))
+                .replaceAll(PATTERN_N,      latLonFormat.format(n))
+                .replaceAll(PATTERN_WIDTH,  String.valueOf(getTileSize()))
+                .replaceAll(PATTERN_HEIGHT, String.valueOf(getTileSize()))
+                .replace(" ", "%20");
+    }
+
+    /**
+     * Checks if url is acceptable by this Tile Source
+     * @param url
+     */
+    public static void checkUrl(String url) {
+        CheckParameterUtil.ensureParameterNotNull(url, "url");
+        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
+        while (m.find()) {
+            boolean isSupportedPattern = false;
+            for (String pattern : ALL_PATTERNS) {
+                if (m.group().matches(pattern)) {
+                    isSupportedPattern = true;
+                    break;
+                }
+            }
+            if (!isSupportedPattern) {
+                throw new IllegalArgumentException(
+                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
+            }
+        }
+    }
+
+    @Override
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
+}
diff --git a/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java b/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java
new file mode 100644
index 0000000..2c1dd82
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java
@@ -0,0 +1,31 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import java.util.Map;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+
+/**
+ * Factory creating TileLoaders for layers
+ *
+ * @author Wiktor Niesiobędzki
+ * @since TODO
+ *
+ */
+public interface TileLoaderFactory {
+
+    /**
+     * @param listener that will be notified, when tile has finished loading
+     * @return TileLoader that notifies specified listener
+     */
+    TileLoader makeTileLoader(TileLoaderListener listener);
+
+    /**
+     * @param listener that will be notified, when tile has finished loading
+     * @param headers that will be sent with requests to TileSource
+     * @return TileLoader that uses both of above
+     */
+    TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
+
+}
diff --git a/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java
new file mode 100644
index 0000000..45b0970
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java
@@ -0,0 +1,47 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+
+/**
+ * Tileloader for WMS based imagery. It is separate to use different ThreadPoolExecutor, as we want
+ * to define number of simultaneous downloads for WMS separately
+ *
+ * @author Wiktor Niesiobędzki
+ * @since TODO
+ *
+ */
+public class WMSCachedTileLoader extends TMSCachedTileLoader {
+
+    /** limit of concurrent connections to WMS tile source (per source) */
+    public static IntegerProperty THREAD_LIMIT = new IntegerProperty("imagery.wms.simultaneousConnections", 3);
+
+    /**
+     * Creates a TileLoader with separate WMS downloader.
+     *
+     * @param listener that will be notified when tile is loaded
+     * @param name name of the cache region
+     * @param connectTimeout to tile source
+     * @param readTimeout from tile source
+     * @param headers to be sent with requests
+     * @param cacheDir place to store the cache
+     * @throws IOException when there is a problem creating cache repository
+     */
+    public WMSCachedTileLoader(TileLoaderListener listener, String name, int connectTimeout, int readTimeout,
+            Map<String, String> headers, String cacheDir) throws IOException {
+
+        super(listener, name, connectTimeout, readTimeout, headers, cacheDir);
+        setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("WMS downloader", THREAD_LIMIT.get()));
+    }
+
+    @Override
+    public TileJob createTileLoaderJob(Tile tile) {
+        return new WMSCachedTileLoaderJob(listener, tile, cache, connectTimeout, readTimeout, headers, getDownloadExecutor());
+    }
+}
diff --git a/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java
new file mode 100644
index 0000000..c9f5356
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java
@@ -0,0 +1,58 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadPoolExecutor;
+
+import org.apache.commons.jcs.access.behavior.ICacheAccess;
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
+
+/**
+ * Separate class to handle WMS jobs, as it needs to react differently to HTTP response codes from WMS server
+ *
+ * @author Wiktor Niesiobędzki
+ * @since TODO
+ *
+ */
+public class WMSCachedTileLoaderJob extends TMSCachedTileLoaderJob {
+
+    /**
+     * Creates a job - that will download specific tile
+     * @param listener will be notified, when tile has loaded
+     * @param tile to load
+     * @param cache to use (get/put)
+     * @param connectTimeout to tile source
+     * @param readTimeout to tile source
+     * @param headers to be sent with request
+     * @param downloadExecutor that will execute the download task (if needed)
+     */
+    public WMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
+            ICacheAccess<String, BufferedImageCacheEntry> cache, int connectTimeout, int readTimeout,
+            Map<String, String> headers, ThreadPoolExecutor downloadExecutor) {
+        super(listener, tile, cache, connectTimeout, readTimeout, headers, downloadExecutor);
+    }
+
+    /**
+     * If 404 is returned from WMS server, treat this as situation as erroneus
+     */
+    @Override
+    protected boolean handleNotFound() {
+        return false;
+    }
+
+    /**
+     * Do not try to cache empty responses
+     */
+    @Override
+    protected boolean cacheAsEmpty(Map<String, List<String>> headers, int statusCode, byte[] content) {
+        if (statusCode < 100 || statusCode > 400) {
+            // cache any errors
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/src/org/openstreetmap/josm/data/imagery/WmsCache.java b/src/org/openstreetmap/josm/data/imagery/WmsCache.java
deleted file mode 100644
index 1223fdf..0000000
--- a/src/org/openstreetmap/josm/data/imagery/WmsCache.java
+++ /dev/null
@@ -1,592 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.data.imagery;
-
-import java.awt.Graphics2D;
-import java.awt.image.BufferedImage;
-import java.io.BufferedOutputStream;
-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.OutputStream;
-import java.lang.ref.SoftReference;
-import java.net.URLConnection;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-
-import javax.imageio.ImageIO;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.Marshaller;
-import javax.xml.bind.Unmarshaller;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.data.ProjectionBounds;
-import org.openstreetmap.josm.data.SystemOfMeasurement;
-import org.openstreetmap.josm.data.coor.EastNorth;
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.imagery.types.EntryType;
-import org.openstreetmap.josm.data.imagery.types.ProjectionType;
-import org.openstreetmap.josm.data.imagery.types.WmsCacheType;
-import org.openstreetmap.josm.data.preferences.StringProperty;
-import org.openstreetmap.josm.data.projection.Projection;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Utils;
-import org.openstreetmap.josm.tools.date.DateUtils;
-
-public class WmsCache {
-    //TODO Property for maximum cache size
-    //TODO Property for maximum age of tile, automatically remove old tiles
-    //TODO Measure time for partially loading from cache, compare with time to download tile. If slower, disable partial cache
-    //TODO Do loading from partial cache and downloading at the same time, don't wait for partial cache to load
-
-    private static final StringProperty PROP_CACHE_PATH = new StringProperty("imagery.wms-cache.path", "wms");
-    private static final String INDEX_FILENAME = "index.xml";
-    private static final String LAYERS_INDEX_FILENAME = "layers.properties";
-
-    private static class CacheEntry {
-        private final double pixelPerDegree;
-        private final double east;
-        private final double north;
-        private final ProjectionBounds bounds;
-        private final String filename;
-
-        private long lastUsed;
-        private long lastModified;
-
-        CacheEntry(double pixelPerDegree, double east, double north, int tileSize, String filename) {
-            this.pixelPerDegree = pixelPerDegree;
-            this.east = east;
-            this.north = north;
-            this.bounds = new ProjectionBounds(east, north, east + tileSize / pixelPerDegree, north + tileSize / pixelPerDegree);
-            this.filename = filename;
-        }
-
-        @Override
-        public String toString() {
-            return "CacheEntry [pixelPerDegree=" + pixelPerDegree + ", east=" + east + ", north=" + north + ", bounds="
-                    + bounds + ", filename=" + filename + ", lastUsed=" + lastUsed + ", lastModified=" + lastModified
-                    + "]";
-        }
-    }
-
-    private static class ProjectionEntries {
-        private final String projection;
-        private final String cacheDirectory;
-        private final List<CacheEntry> entries = new ArrayList<>();
-
-        ProjectionEntries(String projection, String cacheDirectory) {
-            this.projection = projection;
-            this.cacheDirectory = cacheDirectory;
-        }
-    }
-
-    private final Map<String, ProjectionEntries> entries = new HashMap<>();
-    private final File cacheDir;
-    private final int tileSize; // Should be always 500
-    private int totalFileSize;
-    private boolean totalFileSizeDirty; // Some file was missing - size needs to be recalculated
-    // No need for hashCode/equals on CacheEntry, object identity is enough. Comparing by values can lead to error - CacheEntry for wrong projection could be found
-    private Map<CacheEntry, SoftReference<BufferedImage>> memoryCache = new HashMap<>();
-    private Set<ProjectionBounds> areaToCache;
-
-    protected String cacheDirPath() {
-        String cPath = PROP_CACHE_PATH.get();
-        if (!(new File(cPath).isAbsolute())) {
-            cPath = Main.pref.getCacheDirectory() + File.separator + cPath;
-        }
-        return cPath;
-    }
-
-    public WmsCache(String url, int tileSize) {
-        File globalCacheDir = new File(cacheDirPath());
-        if (!globalCacheDir.mkdirs()) {
-            Main.warn("Unable to create global cache directory: "+globalCacheDir.getAbsolutePath());
-        }
-        cacheDir = new File(globalCacheDir, getCacheDirectory(url));
-        cacheDir.mkdirs();
-        this.tileSize = tileSize;
-    }
-
-    private String getCacheDirectory(String url) {
-        String cacheDirName = null;
-        Properties layersIndex = new Properties();
-        File layerIndexFile = new File(cacheDirPath(), LAYERS_INDEX_FILENAME);
-        try (InputStream fis = new FileInputStream(layerIndexFile)) {
-            layersIndex.load(fis);
-        } catch (FileNotFoundException e) {
-            Main.error("Unable to load layers index for wms cache (file " + layerIndexFile + " not found)");
-        } catch (IOException e) {
-            Main.error("Unable to load layers index for wms cache");
-            Main.error(e);
-        }
-
-        for (Object propKey: layersIndex.keySet()) {
-            String s = (String)propKey;
-            if (url.equals(layersIndex.getProperty(s))) {
-                cacheDirName = s;
-                break;
-            }
-        }
-
-        if (cacheDirName == null) {
-            int counter = 0;
-            while (true) {
-                counter++;
-                if (!layersIndex.keySet().contains(String.valueOf(counter))) {
-                    break;
-                }
-            }
-            cacheDirName = String.valueOf(counter);
-            layersIndex.setProperty(cacheDirName, url);
-            try (OutputStream fos = new FileOutputStream(layerIndexFile)) {
-                layersIndex.store(fos, "");
-            } catch (IOException e) {
-                Main.error("Unable to save layer index for wms cache");
-                Main.error(e);
-            }
-        }
-
-        return cacheDirName;
-    }
-
-    private ProjectionEntries getProjectionEntries(Projection projection) {
-        return getProjectionEntries(projection.toCode(), projection.getCacheDirectoryName());
-    }
-
-    private ProjectionEntries getProjectionEntries(String projection, String cacheDirectory) {
-        ProjectionEntries result = entries.get(projection);
-        if (result == null) {
-            result = new ProjectionEntries(projection, cacheDirectory);
-            entries.put(projection, result);
-        }
-
-        return result;
-    }
-
-    public synchronized void loadIndex() {
-        File indexFile = new File(cacheDir, INDEX_FILENAME);
-        try {
-            JAXBContext context = JAXBContext.newInstance(
-                    WmsCacheType.class.getPackage().getName(),
-                    WmsCacheType.class.getClassLoader());
-            Unmarshaller unmarshaller = context.createUnmarshaller();
-            WmsCacheType cacheEntries;
-            try (InputStream is = new FileInputStream(indexFile)) {
-                cacheEntries = (WmsCacheType)unmarshaller.unmarshal(is);
-            }
-            totalFileSize = cacheEntries.getTotalFileSize();
-            if (cacheEntries.getTileSize() != tileSize) {
-                Main.info("Cache created with different tileSize, cache will be discarded");
-                return;
-            }
-            for (ProjectionType projectionType: cacheEntries.getProjection()) {
-                ProjectionEntries projection = getProjectionEntries(projectionType.getName(), projectionType.getCacheDirectory());
-                for (EntryType entry: projectionType.getEntry()) {
-                    CacheEntry ce = new CacheEntry(entry.getPixelPerDegree(), entry.getEast(), entry.getNorth(), tileSize, entry.getFilename());
-                    ce.lastUsed = entry.getLastUsed().getTimeInMillis();
-                    ce.lastModified = entry.getLastModified().getTimeInMillis();
-                    projection.entries.add(ce);
-                }
-            }
-        } catch (Exception e) {
-            if (indexFile.exists()) {
-                Main.error(e);
-                Main.info("Unable to load index for wms-cache, new file will be created");
-            } else {
-                Main.info("Index for wms-cache doesn't exist, new file will be created");
-            }
-        }
-
-        removeNonReferencedFiles();
-    }
-
-    private void removeNonReferencedFiles() {
-
-        Set<String> usedProjections = new HashSet<>();
-
-        for (ProjectionEntries projectionEntries: entries.values()) {
-
-            usedProjections.add(projectionEntries.cacheDirectory);
-
-            File projectionDir = new File(cacheDir, projectionEntries.cacheDirectory);
-            if (projectionDir.exists()) {
-                Set<String> referencedFiles = new HashSet<>();
-
-                for (CacheEntry ce: projectionEntries.entries) {
-                    referencedFiles.add(ce.filename);
-                }
-
-                File[] files = projectionDir.listFiles();
-                if (files != null) {
-                    for (File file: files) {
-                        if (!referencedFiles.contains(file.getName()) && !file.delete()) {
-                            Main.warn("Unable to delete file: "+file.getAbsolutePath());
-                        }
-                    }
-                }
-            }
-        }
-
-        File[] files = cacheDir.listFiles();
-        if (files != null) {
-            for (File projectionDir: files) {
-                if (projectionDir.isDirectory() && !usedProjections.contains(projectionDir.getName())) {
-                    Utils.deleteDirectory(projectionDir);
-                }
-            }
-        }
-    }
-
-    private int calculateTotalFileSize() {
-        int result = 0;
-        for (ProjectionEntries projectionEntries: entries.values()) {
-            Iterator<CacheEntry> it = projectionEntries.entries.iterator();
-            while (it.hasNext()) {
-                CacheEntry entry = it.next();
-                File imageFile = getImageFile(projectionEntries, entry);
-                if (!imageFile.exists()) {
-                    it.remove();
-                } else {
-                    result += imageFile.length();
-                }
-            }
-        }
-        return result;
-    }
-
-    public synchronized void saveIndex() {
-        WmsCacheType index = new WmsCacheType();
-
-        if (totalFileSizeDirty) {
-            totalFileSize = calculateTotalFileSize();
-        }
-
-        index.setTileSize(tileSize);
-        index.setTotalFileSize(totalFileSize);
-        for (ProjectionEntries projectionEntries: entries.values()) {
-            if (!projectionEntries.entries.isEmpty()) {
-                ProjectionType projectionType = new ProjectionType();
-                projectionType.setName(projectionEntries.projection);
-                projectionType.setCacheDirectory(projectionEntries.cacheDirectory);
-                index.getProjection().add(projectionType);
-                for (CacheEntry ce: projectionEntries.entries) {
-                    EntryType entry = new EntryType();
-                    entry.setPixelPerDegree(ce.pixelPerDegree);
-                    entry.setEast(ce.east);
-                    entry.setNorth(ce.north);
-                    Calendar c = Calendar.getInstance();
-                    c.setTimeInMillis(ce.lastUsed);
-                    entry.setLastUsed(c);
-                    c = Calendar.getInstance();
-                    c.setTimeInMillis(ce.lastModified);
-                    entry.setLastModified(c);
-                    entry.setFilename(ce.filename);
-                    projectionType.getEntry().add(entry);
-                }
-            }
-        }
-        try {
-            JAXBContext context = JAXBContext.newInstance(
-                    WmsCacheType.class.getPackage().getName(),
-                    WmsCacheType.class.getClassLoader());
-            Marshaller marshaller = context.createMarshaller();
-            try (OutputStream fos = new FileOutputStream(new File(cacheDir, INDEX_FILENAME))) {
-                marshaller.marshal(index, fos);
-            }
-        } catch (Exception e) {
-            Main.error("Failed to save wms-cache file");
-            Main.error(e);
-        }
-    }
-
-    private File getImageFile(ProjectionEntries projection, CacheEntry entry) {
-        return new File(cacheDir, projection.cacheDirectory + "/" + entry.filename);
-    }
-
-    private BufferedImage loadImage(ProjectionEntries projectionEntries, CacheEntry entry, boolean enforceTransparency) throws IOException {
-        synchronized (this) {
-            entry.lastUsed = System.currentTimeMillis();
-
-            SoftReference<BufferedImage> memCache = memoryCache.get(entry);
-            if (memCache != null) {
-                BufferedImage result = memCache.get();
-                if (result != null) {
-                    if (enforceTransparency == ImageProvider.isTransparencyForced(result)) {
-                        return result;
-                    } else if (Main.isDebugEnabled()) {
-                        Main.debug("Skipping "+entry+" from memory cache (transparency enforcement)");
-                    }
-                }
-            }
-        }
-
-        try {
-            // Reading can't be in synchronized section, it's too slow
-            BufferedImage result = ImageProvider.read(getImageFile(projectionEntries, entry), true, enforceTransparency);
-            synchronized (this) {
-                if (result == null) {
-                    projectionEntries.entries.remove(entry);
-                    totalFileSizeDirty = true;
-                }
-                return result;
-            }
-        } catch (IOException e) {
-            synchronized (this) {
-                projectionEntries.entries.remove(entry);
-                totalFileSizeDirty = true;
-                throw e;
-            }
-        }
-    }
-
-    private CacheEntry findEntry(ProjectionEntries projectionEntries, double pixelPerDegree, double east, double north) {
-        for (CacheEntry entry: projectionEntries.entries) {
-            if (Utils.equalsEpsilon(entry.pixelPerDegree, pixelPerDegree)
-                    && Utils.equalsEpsilon(entry.east, east) && Utils.equalsEpsilon(entry.north, north))
-                return entry;
-        }
-        return null;
-    }
-
-    public synchronized boolean hasExactMatch(Projection projection, double pixelPerDegree, double east, double north) {
-        ProjectionEntries projectionEntries = getProjectionEntries(projection);
-        return findEntry(projectionEntries, pixelPerDegree, east, north) != null;
-    }
-
-    public BufferedImage getExactMatch(Projection projection, double pixelPerDegree, double east, double north) {
-        CacheEntry entry = null;
-        ProjectionEntries projectionEntries = null;
-        synchronized (this) {
-            projectionEntries = getProjectionEntries(projection);
-            entry = findEntry(projectionEntries, pixelPerDegree, east, north);
-        }
-        if (entry != null) {
-            try {
-                return loadImage(projectionEntries, entry, WMSLayer.PROP_ALPHA_CHANNEL.get());
-            } catch (IOException e) {
-                Main.error("Unable to load file from wms cache");
-                Main.error(e);
-                return null;
-            }
-        }
-        return null;
-    }
-
-    public BufferedImage getPartialMatch(Projection projection, double pixelPerDegree, double east, double north) {
-        ProjectionEntries projectionEntries;
-        List<CacheEntry> matches;
-        synchronized (this) {
-            matches = new ArrayList<>();
-
-            double minPPD = pixelPerDegree / 5;
-            double maxPPD = pixelPerDegree * 5;
-            projectionEntries = getProjectionEntries(projection);
-
-            double size2 = tileSize / pixelPerDegree;
-            double border = tileSize * 0.01; // Make sure not to load neighboring tiles that intersects this tile only slightly
-            ProjectionBounds bounds = new ProjectionBounds(east + border, north + border,
-                    east + size2 - border, north + size2 - border);
-
-            //TODO Do not load tile if it is completely overlapped by other tile with better ppd
-            for (CacheEntry entry: projectionEntries.entries) {
-                if (entry.pixelPerDegree >= minPPD && entry.pixelPerDegree <= maxPPD && entry.bounds.intersects(bounds)) {
-                    entry.lastUsed = System.currentTimeMillis();
-                    matches.add(entry);
-                }
-            }
-
-            if (matches.isEmpty())
-                return null;
-
-            Collections.sort(matches, new Comparator<CacheEntry>() {
-                @Override
-                public int compare(CacheEntry o1, CacheEntry o2) {
-                    return Double.compare(o2.pixelPerDegree, o1.pixelPerDegree);
-                }
-            });
-        }
-
-        // Use alpha layer only when enabled on wms layer
-        boolean alpha = WMSLayer.PROP_ALPHA_CHANNEL.get();
-        BufferedImage result = new BufferedImage(tileSize, tileSize,
-                alpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR);
-        Graphics2D g = result.createGraphics();
-
-        boolean drawAtLeastOnce = false;
-        Map<CacheEntry, SoftReference<BufferedImage>> localCache = new HashMap<>();
-        for (CacheEntry ce: matches) {
-            BufferedImage img;
-            try {
-                // Enforce transparency only when alpha enabled on wms layer too
-                img = loadImage(projectionEntries, ce, alpha);
-                localCache.put(ce, new SoftReference<>(img));
-            } catch (IOException e) {
-                continue;
-            }
-
-            drawAtLeastOnce = true;
-
-            int xDiff = (int)((ce.east - east) * pixelPerDegree);
-            int yDiff = (int)((ce.north - north) * pixelPerDegree);
-            int size = (int)(pixelPerDegree / ce.pixelPerDegree  * tileSize);
-
-            int x = xDiff;
-            int y = -size + tileSize - yDiff;
-
-            g.drawImage(img, x, y, size, size, null);
-        }
-
-        if (drawAtLeastOnce) {
-            synchronized (this) {
-                memoryCache.putAll(localCache);
-            }
-            return result;
-        } else
-            return null;
-    }
-
-    private String generateFileName(ProjectionEntries projectionEntries, double pixelPerDegree, Projection projection, double east, double north, String mimeType) {
-        LatLon ll1 = projection.eastNorth2latlon(new EastNorth(east, north));
-        LatLon ll2 = projection.eastNorth2latlon(new EastNorth(east + 100 / pixelPerDegree, north));
-        LatLon ll3 = projection.eastNorth2latlon(new EastNorth(east + tileSize / pixelPerDegree, north + tileSize / pixelPerDegree));
-
-        double deltaLat = Math.abs(ll3.lat() - ll1.lat());
-        double deltaLon = Math.abs(ll3.lon() - ll1.lon());
-        int precisionLat = Math.max(0, -(int)Math.ceil(Math.log10(deltaLat)) + 1);
-        int precisionLon = Math.max(0, -(int)Math.ceil(Math.log10(deltaLon)) + 1);
-
-        String zoom = SystemOfMeasurement.METRIC.getDistText(ll1.greatCircleDistance(ll2));
-        String extension = "dat";
-        if (mimeType != null) {
-            switch(mimeType) {
-            case "image/jpeg":
-            case "image/jpg":
-                extension = "jpg";
-                break;
-            case "image/png":
-                extension = "png";
-                break;
-            case "image/gif":
-                extension = "gif";
-                break;
-            default:
-                Main.warn("Unrecognized MIME type: "+mimeType);
-            }
-        }
-
-        int counter = 0;
-        FILENAME_LOOP:
-            while (true) {
-                String result = String.format("%s_%." + precisionLat + "f_%." + precisionLon +"f%s.%s", zoom, ll1.lat(), ll1.lon(), counter==0?"":"_" + counter, extension);
-                for (CacheEntry entry: projectionEntries.entries) {
-                    if (entry.filename.equals(result)) {
-                        counter++;
-                        continue FILENAME_LOOP;
-                    }
-                }
-                return result;
-            }
-    }
-
-    /**
-     *
-     * @param img Used only when overlapping is used, when not used, used raw from imageData
-     * @param imageData
-     * @param projection
-     * @param pixelPerDegree
-     * @param east
-     * @param north
-     * @throws IOException
-     */
-    public synchronized void saveToCache(BufferedImage img, InputStream imageData, Projection projection, double pixelPerDegree, double east, double north) throws IOException {
-        ProjectionEntries projectionEntries = getProjectionEntries(projection);
-        CacheEntry entry = findEntry(projectionEntries, pixelPerDegree, east, north);
-        File imageFile;
-        if (entry == null) {
-
-            String mimeType;
-            if (img != null) {
-                mimeType = "image/png";
-            } else {
-                mimeType = URLConnection.guessContentTypeFromStream(imageData);
-            }
-            entry = new CacheEntry(pixelPerDegree, east, north, tileSize,generateFileName(projectionEntries, pixelPerDegree, projection, east, north, mimeType));
-            entry.lastUsed = System.currentTimeMillis();
-            entry.lastModified = entry.lastUsed;
-            projectionEntries.entries.add(entry);
-            imageFile = getImageFile(projectionEntries, entry);
-        } else {
-            imageFile = getImageFile(projectionEntries, entry);
-            totalFileSize -= imageFile.length();
-        }
-
-        if (!imageFile.getParentFile().mkdirs()) {
-            Main.warn("Unable to create parent directory: "+imageFile.getParentFile().getAbsolutePath());
-        }
-
-        if (img != null) {
-            BufferedImage copy = new BufferedImage(tileSize, tileSize, img.getType());
-            copy.createGraphics().drawImage(img, 0, 0, tileSize, tileSize, 0, img.getHeight() - tileSize, tileSize, img.getHeight(), null);
-            ImageIO.write(copy, "png", imageFile);
-            totalFileSize += imageFile.length();
-        } else {
-            try (OutputStream os = new BufferedOutputStream(new FileOutputStream(imageFile))) {
-                totalFileSize += Utils.copyStream(imageData, os);
-            }
-        }
-    }
-
-    public synchronized void cleanSmallFiles(int size) {
-        for (ProjectionEntries projectionEntries: entries.values()) {
-            Iterator<CacheEntry> it = projectionEntries.entries.iterator();
-            while (it.hasNext()) {
-                File file = getImageFile(projectionEntries, it.next());
-                long length = file.length();
-                if (length <= size) {
-                    if (length == 0) {
-                        totalFileSizeDirty = true; // File probably doesn't exist
-                    }
-                    totalFileSize -= size;
-                    if (!file.delete()) {
-                        Main.warn("Unable to delete file: "+file.getAbsolutePath());
-                    }
-                    it.remove();
-                }
-            }
-        }
-    }
-
-    public static String printDate(Calendar c) {
-        return DateUtils.newIsoDateFormat().format(c.getTime());
-    }
-
-    private boolean isInsideAreaToCache(CacheEntry cacheEntry) {
-        for (ProjectionBounds b: areaToCache) {
-            if (cacheEntry.bounds.intersects(b))
-                return true;
-        }
-        return false;
-    }
-
-    public synchronized void setAreaToCache(Set<ProjectionBounds> areaToCache) {
-        this.areaToCache = areaToCache;
-        Iterator<CacheEntry> it = memoryCache.keySet().iterator();
-        while (it.hasNext()) {
-            if (!isInsideAreaToCache(it.next())) {
-                it.remove();
-            }
-        }
-    }
-}
diff --git a/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java b/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
index 3effadd..09b976c 100644
--- a/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
+++ b/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
@@ -29,6 +29,7 @@ import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
 import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
 import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource;
 import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource;
 import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
@@ -69,7 +70,7 @@ public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser {
                     continue;
                 }
                 try {
-                    TileSource source = TMSLayer.getTileSource(info);
+                    TileSource source = AbstractTMSTileSource.getTileSource(info);
                     if (source != null) {
                         sources.add(source);
                     }
diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
new file mode 100644
index 0000000..c8b7fc1
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
@@ -0,0 +1,1635 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridBagLayout;
+import java.awt.Image;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.image.ImageObserver;
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.BorderFactory;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JLabel;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JTextField;
+
+import org.openstreetmap.gui.jmapviewer.AttributionSupport;
+import org.openstreetmap.gui.jmapviewer.Coordinate;
+import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
+import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
+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.josm.Main;
+import org.openstreetmap.josm.actions.RenameLayerAction;
+import org.openstreetmap.josm.actions.SaveActionBase;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
+import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
+import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
+import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.WMSLayerImporter;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ *
+ * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
+ *
+ * It implements all standard functions of tilesource based layers: autozoom,  tile reloads, layer saving, loading,etc.
+ *
+ * @author Wiktor Niesiobędzki
+ * @since TODO
+ *
+ */
+public abstract class AbstractTileSourceLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
+    private static final String PREFERENCE_PREFIX   = "imagery.generic";
+
+    /** maximum zoom level supported */
+    public static final int MAX_ZOOM = 30;
+    /** minium zoom level supported */
+    public static final int MIN_ZOOM = 2;
+    private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
+
+    /** do set autozoom when creating a new layer */
+    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
+    /** do set autoload when creating a new layer */
+    public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
+    /** do set showerrors when creating a new layer */
+    public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true);
+    /** minimum zoom level to show to user */
+    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2);
+    /** maximum zoom level to show to user */
+    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20);
+
+    //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
+    /**
+     * Zoomlevel at which tiles is currently downloaded.
+     * Initial zoom lvl is set to bestZoom
+     */
+    public int currentZoomLevel;
+    private boolean needRedraw;
+
+    private AttributionSupport attribution = new AttributionSupport();
+    Tile showMetadataTile;
+
+    // needed public access for session exporter
+    /** if layers changes automatically, when user zooms in */
+    public boolean autoZoom;
+    /** if layer automatically loads new tiles */
+    public boolean autoLoad;
+    /** if layer should show errors on tiles */
+    public boolean showErrors;
+
+    protected TileCache tileCache;
+    protected TileSource tileSource;
+    protected TileLoader tileLoader;
+
+    /**
+     * Creates Tile Source based Imagery Layer based on Imagery Info
+     * @param info
+     */
+    public AbstractTileSourceLayer(ImageryInfo info) {
+        super(info);
+
+        if(!isProjectionSupported(Main.getProjection())) {
+            JOptionPane.showMessageDialog(Main.parent,
+                    tr("This layer do not support the projection {0}.\n{1}\n"
+                            + "Change the projection or remove the layer.",
+                            Main.getProjection().toCode(), nameSupportedProjections()),
+                            tr("Warning"),
+                            JOptionPane.WARNING_MESSAGE);
+        }
+        setBackgroundLayer(true);
+        this.setVisible(true);
+
+        initTileSource(getTileSource(info));
+        MapView.addZoomChangeListener(this);
+    }
+
+    protected abstract TileLoaderFactory getTileLoaderFactory();
+
+    /**
+     *
+     * @param info
+     * @return TileSource for specified ImageryInfo
+     * @throws IllegalArgumentException when Imagery is not supported by layer
+     */
+    protected abstract TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException;
+
+    protected abstract Map<String, String> getHeaders(TileSource tileSource);
+
+    protected void initTileSource(TileSource tileSource) {
+        this.tileSource = tileSource;
+        attribution.initialize(tileSource);
+
+        currentZoomLevel = getBestZoom();
+
+        Map<String, String> headers = getHeaders(tileSource);
+
+        tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
+        if (tileLoader instanceof TMSCachedTileLoader) {
+            tileCache = (TileCache) tileLoader;
+        } else {
+            tileCache = new MemoryTileCache();
+        }
+        if (tileLoader == null)
+            tileLoader = new OsmTileLoader(this);
+    }
+
+
+
+    @Override
+    public synchronized void tileLoadingFinished(Tile tile, boolean success) {
+        if (tile.hasError()) {
+            success = false;
+            tile.setImage(null);
+        }
+        if (sharpenLevel != 0 && success) {
+            tile.setImage(sharpenImage(tile.getImage()));
+        }
+        tile.setLoaded(success);
+        needRedraw = true;
+        if (Main.map != null) {
+            Main.map.repaint(100);
+        }
+        if (Main.isDebugEnabled()) {
+            Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
+        }
+    }
+
+    /**
+     * Clears the tile cache.
+     *
+     * If the current tileLoader is an instance of OsmTileLoader, a new
+     * TmsTileClearController is created and passed to the according clearCache
+     * method.
+     *
+     * @param monitor not used in this implementation - as cache clear is instaneus
+     */
+    public void clearTileCache(ProgressMonitor monitor) {
+        tileCache.clear();
+        if (tileLoader instanceof CachedTileLoader) {
+            ((CachedTileLoader)tileLoader).clearCache(tileSource);
+        }
+    }
+
+
+    /**
+     * Initiates a repaint of Main.map
+     *
+     * @see Main#map
+     * @see MapFrame#repaint()
+     */
+    protected void redraw() {
+        needRedraw = true;
+        Main.map.repaint();
+    }
+
+    /**
+     * Marks layer as needing redraw on offset change
+     */
+    @Override
+    public void setOffset(double dx, double dy) {
+        super.setOffset(dx, dy);
+        needRedraw = true;
+    }
+
+
+    /**
+     * Returns average number of screen pixels per tile pixel for current mapview
+     */
+    private double getScaleFactor(int zoom) {
+        if (!Main.isDisplayingMapView()) return 1;
+        MapView mv = Main.map.mapView;
+        LatLon topLeft = mv.getLatLon(0, 0);
+        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+        double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
+        double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
+        double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
+        double y2 = tileSource.latToTileY(botRight.lat(), zoom);
+
+        int screenPixels = mv.getWidth()*mv.getHeight();
+        double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
+        if (screenPixels == 0 || tilePixels == 0) return 1;
+        return screenPixels/tilePixels;
+    }
+
+    private final int getBestZoom() {
+        double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
+        double result = Math.log(factor)/Math.log(2)/2+1;
+        /*
+         * Math.log(factor)/Math.log(2) - gives log base 2 of factor
+         * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
+         * In general, smaller zoom levels are more readable.  We prefer big,
+         * block, pixelated (but readable) map text to small, smeared,
+         * unreadable underzoomed text.  So, use .floor() instead of rounding
+         * to skew things a bit toward the lower zooms.
+         * Remember, that result here, should correspond to TMSLayer.paint(...)
+         * getScaleFactor(...) is supposed to be between 0.75 and 3
+         */
+        int intResult = (int)Math.floor(result);
+        if (intResult > getMaxZoomLvl())
+            return getMaxZoomLvl();
+        if (intResult < getMinZoomLvl())
+            return getMinZoomLvl();
+        return intResult;
+    }
+
+
+    private final static boolean actionSupportLayers(List<Layer> layers) {
+        return layers.size() == 1 && layers.get(0) instanceof TMSLayer;
+    }
+
+    private class AutoZoomAction extends AbstractAction implements LayerAction {
+        public AutoZoomAction() {
+            super(tr("Auto Zoom"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ae) {
+            autoZoom = !autoZoom;
+        }
+
+        public Component createMenuComponent() {
+            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
+            item.setSelected(autoZoom);
+            return item;
+        }
+        @Override
+        public boolean supportLayers(List<Layer> layers) {
+            return actionSupportLayers(layers);
+        }
+
+    }
+
+    private class AutoLoadTilesAction extends AbstractAction implements LayerAction {
+        public AutoLoadTilesAction() {
+            super(tr("Auto load tiles"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ae) {
+            autoLoad= !autoLoad;
+        }
+
+        public Component createMenuComponent() {
+            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
+            item.setSelected(autoLoad);
+            return item;
+        }
+        @Override
+        public boolean supportLayers(List<Layer> layers) {
+            return actionSupportLayers(layers);
+        }
+    }
+
+    private class LoadAllTilesAction extends AbstractAction {
+        public LoadAllTilesAction() {
+            super(tr("Load All Tiles"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ae) {
+            loadAllTiles(true);
+            redraw();
+        }
+    }
+
+    private class LoadErroneusTilesAction extends AbstractAction {
+        public LoadErroneusTilesAction() {
+            super(tr("Load All Error Tiles"));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent ae) {
+            loadAllErrorTiles(true);
+            redraw();
+        }
+    }
+
+    private class ZoomToNativeLevelAction extends AbstractAction {
+        public ZoomToNativeLevelAction() {
+            super(tr("Zoom to native resolution"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ae) {
+            double new_factor = Math.sqrt(getScaleFactor(currentZoomLevel));
+            Main.map.mapView.zoomToFactor(new_factor);
+            redraw();
+        }
+    }
+
+    private class ZoomToBestAction extends AbstractAction {
+        public ZoomToBestAction() {
+            super(tr("Change resolution"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ae) {
+            setZoomLevel(getBestZoom());
+        }
+    }
+
+    /*
+     * Simple class to keep clickedTile within hookUpMapView
+     */
+    private class TileHolder {
+        private Tile t = null;
+
+        public Tile getTile() {
+            return t;
+        }
+
+        public void setTile(Tile t) {
+            this.t = t;
+        }
+    }
+
+    /**
+     * Creates popup menu items and binds to mouse actions
+     */
+    @Override
+    public void hookUpMapView() {
+        // keep them final here, so we avoid namespace clutter in the class
+        final JPopupMenu tileOptionMenu = new JPopupMenu();
+        final TileHolder clickedTileHolder = new TileHolder();
+
+        autoZoom = PROP_DEFAULT_AUTOZOOM.get();
+        JCheckBoxMenuItem autoZoomPopup = new JCheckBoxMenuItem();
+        autoZoomPopup.setAction(new AutoZoomAction());
+        autoZoomPopup.setSelected(autoZoom);
+        tileOptionMenu.add(autoZoomPopup);
+
+        autoLoad = PROP_DEFAULT_AUTOLOAD.get();
+        JCheckBoxMenuItem autoLoadPopup = new JCheckBoxMenuItem();
+        autoLoadPopup.setAction(new AutoLoadTilesAction());
+        autoLoadPopup.setSelected(autoLoad);
+        tileOptionMenu.add(autoLoadPopup);
+
+        showErrors = PROP_DEFAULT_SHOWERRORS.get();
+        JCheckBoxMenuItem showErrorsPopup = new JCheckBoxMenuItem();
+        showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                showErrors = !showErrors;
+            }
+        });
+        showErrorsPopup.setSelected(showErrors);
+        tileOptionMenu.add(showErrorsPopup);
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                Tile clickedTile = clickedTileHolder.getTile();
+                if (clickedTile != null) {
+                    loadTile(clickedTile, true);
+                    redraw();
+                }
+            }
+        }));
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Show Tile Info")) {
+            private String getSizeString(int size) {
+                StringBuilder ret = new StringBuilder();
+                return ret.append(size).append("x").append(size).toString();
+            }
+
+            private JTextField createTextField(String text) {
+                JTextField ret = new JTextField(text);
+                ret.setEditable(false);
+                ret.setBorder(BorderFactory.createEmptyBorder());
+                return ret;
+            }
+
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                Tile clickedTile = clickedTileHolder.getTile();
+                if (clickedTile != null) {
+                    ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
+                    JPanel panel = new JPanel(new GridBagLayout());
+                    Rectangle displaySize = tileToRect(clickedTile);
+                    String url = "";
+                    try {
+                        url = clickedTile.getUrl();
+                    } catch (IOException e) {
+                        // silence exceptions
+                    }
+
+                    String[][] content = {
+                            {"Tile name", clickedTile.getKey()},
+                            {"Tile url", url},
+                            {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
+                            {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
+                    };
+
+                    for (String[] entry: content) {
+                        panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
+                        panel.add(GBC.glue(5,0), GBC.std());
+                        panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
+                    }
+
+                    for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
+                        panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
+                        panel.add(GBC.glue(5,0), GBC.std());
+                        String value = e.getValue();
+                        if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
+                            value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
+                        }
+                        panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
+
+                    }
+                    ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
+                    ed.setContent(panel);
+                    ed.showDialog();
+                }
+            }
+        }));
+
+        tileOptionMenu.add(new JMenuItem(new LoadAllTilesAction()));
+        tileOptionMenu.add(new JMenuItem(new LoadErroneusTilesAction()));
+
+        // increase and decrease commands
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Increase zoom")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                increaseZoomLevel();
+                redraw();
+            }
+        }));
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Decrease zoom")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                decreaseZoomLevel();
+                redraw();
+            }
+        }));
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Snap to tile size")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
+                Main.map.mapView.zoomToFactor(newFactor);
+                redraw();
+            }
+        }));
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Flush Tile Cache")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                new PleaseWaitRunnable(tr("Flush Tile Cache")) {
+                    @Override
+                    protected void realRun() {
+                        clearTileCache(getProgressMonitor());
+                    }
+
+                    @Override
+                    protected void finish() {
+                    }
+
+                    @Override
+                    protected void cancel() {
+                    }
+                }.run();
+            }
+        }));
+
+        final MouseAdapter adapter = new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                if (!isVisible()) return;
+                if (e.getButton() == MouseEvent.BUTTON3) {
+                    clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));
+                    tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
+                } else if (e.getButton() == MouseEvent.BUTTON1) {
+                    attribution.handleAttribution(e.getPoint(), true);
+                }
+            }
+        };
+        Main.map.mapView.addMouseListener(adapter);
+
+        MapView.addLayerChangeListener(new LayerChangeListener() {
+            @Override
+            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+                //
+            }
+
+            @Override
+            public void layerAdded(Layer newLayer) {
+                //
+            }
+
+            @Override
+            public void layerRemoved(Layer oldLayer) {
+                if (oldLayer == AbstractTileSourceLayer.this) {
+                    Main.map.mapView.removeMouseListener(adapter);
+                    MapView.removeLayerChangeListener(this);
+                    MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
+                }
+            }
+        });
+    }
+
+    /**
+     * Checks zoom level against settings
+     * @param maxZoomLvl zoom level to check
+     * @param ts tile source to crosscheck with
+     * @return maximum zoom level, not higher than supported by tilesource nor set by the user
+     */
+    public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
+        if(maxZoomLvl > MAX_ZOOM) {
+            maxZoomLvl = MAX_ZOOM;
+        }
+        if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
+            maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
+        }
+        if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
+            maxZoomLvl = ts.getMaxZoom();
+        }
+        return maxZoomLvl;
+    }
+
+    /**
+     * Checks zoom level against settings
+     * @param minZoomLvl zoom level to check
+     * @param ts tile source to crosscheck with
+     * @return minimum zoom level, not higher than supported by tilesource nor set by the user
+     */
+    public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
+        if(minZoomLvl < MIN_ZOOM) {
+            minZoomLvl = MIN_ZOOM;
+        }
+        if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
+            minZoomLvl = getMaxZoomLvl(ts);
+        }
+        if (ts != null && ts.getMinZoom() > minZoomLvl) {
+            minZoomLvl = ts.getMinZoom();
+        }
+        return minZoomLvl;
+    }
+
+
+    /**
+     * @param ts TileSource for which we want to know maximum zoom level
+     * @return maximum max zoom level, that will be shown on layer
+     */
+    public static int getMaxZoomLvl(TileSource ts) {
+        return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
+    }
+
+    /**
+     * @param ts TileSource for which we want to know minimum zoom level
+     * @return minimum zoom level, that will be shown on layer
+     */
+    public static int getMinZoomLvl(TileSource ts) {
+        return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
+    }
+
+
+    /**
+     * Sets maximum zoom level, that layer will attempt show
+     * @param maxZoomLvl
+     */
+    public static void setMaxZoomLvl(int maxZoomLvl) {
+        maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null);
+        PROP_MAX_ZOOM_LVL.put(maxZoomLvl);
+    }
+
+    /**
+     * Sets minimum zoom level, that layer will attempt show
+     * @param minZoomLvl
+     */
+    public static void setMinZoomLvl(int minZoomLvl) {
+        minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
+        PROP_MIN_ZOOM_LVL.put(minZoomLvl);
+    }
+
+
+    /**
+     * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
+     * changes to visible map (panning/zooming)
+     */
+    @Override
+    public void zoomChanged() {
+        if (Main.isDebugEnabled()) {
+            Main.debug("zoomChanged(): " + currentZoomLevel);
+        }
+        if (tileLoader instanceof TMSCachedTileLoader) {
+            ((TMSCachedTileLoader)tileLoader).cancelOutstandingTasks();
+        }
+        needRedraw = true;
+    }
+
+    protected int getMaxZoomLvl() {
+        if (info.getMaxZoom() != 0)
+            return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
+        else
+            return getMaxZoomLvl(tileSource);
+    }
+
+    protected int getMinZoomLvl() {
+        return getMinZoomLvl(tileSource);
+    }
+
+    /**
+     *
+     * @return if its allowed to zoom in
+     */
+    public boolean zoomIncreaseAllowed() {
+        boolean zia = currentZoomLevel < this.getMaxZoomLvl();
+        if (Main.isDebugEnabled()) {
+            Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
+        }
+        return zia;
+    }
+
+    /**
+     * Zoom in, go closer to map.
+     *
+     * @return    true, if zoom increasing was successful, false otherwise
+     */
+    public boolean increaseZoomLevel() {
+        if (zoomIncreaseAllowed()) {
+            currentZoomLevel++;
+            if (Main.isDebugEnabled()) {
+                Main.debug("increasing zoom level to: " + currentZoomLevel);
+            }
+            zoomChanged();
+        } else {
+            Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
+                    "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Sets the zoom level of the layer
+     * @param zoom zoom level
+     * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
+     */
+    public boolean setZoomLevel(int zoom) {
+        if (zoom == currentZoomLevel) return true;
+        if (zoom > this.getMaxZoomLvl()) return false;
+        if (zoom < this.getMinZoomLvl()) return false;
+        currentZoomLevel = zoom;
+        zoomChanged();
+        return true;
+    }
+
+    /**
+     * Check if zooming out is allowed
+     *
+     * @return    true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
+     */
+    public boolean zoomDecreaseAllowed() {
+        return currentZoomLevel > this.getMinZoomLvl();
+    }
+
+    /**
+     * Zoom out from map.
+     *
+     * @return    true, if zoom increasing was successfull, false othervise
+     */
+    public boolean decreaseZoomLevel() {
+        //int minZoom = this.getMinZoomLvl();
+        if (zoomDecreaseAllowed()) {
+            if (Main.isDebugEnabled()) {
+                Main.debug("decreasing zoom level to: " + currentZoomLevel);
+            }
+            currentZoomLevel--;
+            zoomChanged();
+        } else {
+            /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/
+            return false;
+        }
+        return true;
+    }
+
+    /*
+     * We use these for quick, hackish calculations.  They
+     * are temporary only and intentionally not inserted
+     * into the tileCache.
+     */
+    private Tile tempCornerTile(Tile t) {
+        int x = t.getXtile() + 1;
+        int y = t.getYtile() + 1;
+        int zoom = t.getZoom();
+        Tile tile = getTile(x, y, zoom);
+        if (tile != null)
+            return tile;
+        return new Tile(tileSource, x, y, zoom);
+    }
+
+    private Tile getOrCreateTile(int x, int y, int zoom) {
+        Tile tile = getTile(x, y, zoom);
+        if (tile == null) {
+            tile = new Tile(tileSource, x, y, zoom);
+            tileCache.addTile(tile);
+            tile.loadPlaceholderFromCache(tileCache);
+        }
+        return tile;
+    }
+
+    /*
+     * This can and will return null for tiles that are not
+     * already in the cache.
+     */
+    private Tile getTile(int x, int y, int zoom) {
+        int max = (1 << zoom);
+        if (x < 0 || x >= max || y < 0 || y >= max)
+            return null;
+        return tileCache.getTile(tileSource, x, y, zoom);
+    }
+
+    private boolean loadTile(Tile tile, boolean force) {
+        if (tile == null)
+            return false;
+        if (!force && (tile.isLoaded() || tile.hasError()))
+            return false;
+        if (tile.isLoading())
+            return false;
+        tileLoader.createTileLoaderJob(tile).submit();
+        return true;
+    }
+
+    private TileSet getVisibleTileSet() {
+        MapView mv = Main.map.mapView;
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
+        return new TileSet(topLeft, botRight, currentZoomLevel);
+    }
+
+    private void loadAllTiles(boolean force) {
+        TileSet ts = getVisibleTileSet();
+
+        // if there is more than 18 tiles on screen in any direction, do not
+        // load all tiles!
+        if (ts.tooLarge()) {
+            Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
+            return;
+        }
+        ts.loadAllTiles(force);
+    }
+
+    private void loadAllErrorTiles(boolean force) {
+        TileSet ts = getVisibleTileSet();
+        ts.loadAllErrorTiles(force);
+    }
+
+    @Override
+    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
+        boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
+        needRedraw = true;
+        if (Main.isDebugEnabled()) {
+            Main.debug("imageUpdate() done: " + done + " calling repaint");
+        }
+        Main.map.repaint(done ? 0 : 100);
+        return !done;
+    }
+
+    private boolean imageLoaded(Image i) {
+        if (i == null)
+            return false;
+        int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
+        if ((status & ALLBITS) != 0)
+            return true;
+        return false;
+    }
+
+    /**
+     * Returns the image for the given tile if both tile and image are loaded.
+     * Otherwise returns  null.
+     *
+     * @param tile the Tile for which the image should be returned
+     * @return  the image of the tile or null.
+     */
+    private Image getLoadedTileImage(Tile tile) {
+        if (!tile.isLoaded())
+            return null;
+        Image img = tile.getImage();
+        if (!imageLoaded(img))
+            return null;
+        return img;
+    }
+
+    private LatLon tileLatLon(Tile t) {
+        int zoom = t.getZoom();
+        return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),
+                tileSource.tileXToLon(t.getXtile(), zoom));
+    }
+
+    private Rectangle tileToRect(Tile t1) {
+        /*
+         * We need to get a box in which to draw, so advance by one tile in
+         * each direction to find the other corner of the box.
+         * Note: this somewhat pollutes the tile cache
+         */
+        Tile t2 = tempCornerTile(t1);
+        Rectangle rect = new Rectangle(pixelPos(t1));
+        rect.add(pixelPos(t2));
+        return rect;
+    }
+
+    // 'source' is the pixel coordinates for the area that
+    // the img is capable of filling in.  However, we probably
+    // only want a portion of it.
+    //
+    // 'border' is the screen cordinates that need to be drawn.
+    //  We must not draw outside of it.
+    private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
+        Rectangle target = source;
+
+        // If a border is specified, only draw the intersection
+        // if what we have combined with what we are supposed
+        // to draw.
+        if (border != null) {
+            target = source.intersection(border);
+            if (Main.isDebugEnabled()) {
+                Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
+            }
+        }
+
+        // All of the rectangles are in screen coordinates.  We need
+        // to how these correlate to the sourceImg pixels.  We could
+        // avoid doing this by scaling the image up to the 'source' size,
+        // but this should be cheaper.
+        //
+        // In some projections, x any y are scaled differently enough to
+        // cause a pixel or two of fudge.  Calculate them separately.
+        double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
+        double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
+
+        // How many pixels into the 'source' rectangle are we drawing?
+        int screen_x_offset = target.x - source.x;
+        int screen_y_offset = target.y - source.y;
+        // And how many pixels into the image itself does that
+        // correlate to?
+        int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5);
+        int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5);
+        // Now calculate the other corner of the image that we need
+        // by scaling the 'target' rectangle's dimensions.
+        int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5);
+        int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5);
+
+        if (Main.isDebugEnabled()) {
+            Main.debug("drawing image into target rect: " + target);
+        }
+        g.drawImage(sourceImg,
+                target.x, target.y,
+                target.x + target.width, target.y + target.height,
+                img_x_offset, img_y_offset,
+                img_x_end, img_y_end,
+                this);
+        if (PROP_FADE_AMOUNT.get() != 0) {
+            // dimm by painting opaque rect...
+            g.setColor(getFadeColorWithAlpha());
+            g.fillRect(target.x, target.y,
+                    target.width, target.height);
+        }
+    }
+
+    // This function is called for several zoom levels, not just
+    // the current one.  It should not trigger any tiles to be
+    // downloaded.  It should also avoid polluting the tile cache
+    // with any tiles since these tiles are not mandatory.
+    //
+    // The "border" tile tells us the boundaries of where we may
+    // draw.  It will not be from the zoom level that is being
+    // drawn currently.  If drawing the displayZoomLevel,
+    // border is null and we draw the entire tile set.
+    private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
+        if (zoom <= 0) return Collections.emptyList();
+        Rectangle borderRect = null;
+        if (border != null) {
+            borderRect = tileToRect(border);
+        }
+        List<Tile> missedTiles = new LinkedList<>();
+        // The callers of this code *require* that we return any tiles
+        // that we do not draw in missedTiles.  ts.allExistingTiles() by
+        // default will only return already-existing tiles.  However, we
+        // need to return *all* tiles to the callers, so force creation
+        // here.
+        //boolean forceTileCreation = true;
+        for (Tile tile : ts.allTilesCreate()) {
+            Image img = getLoadedTileImage(tile);
+            if (img == null || tile.hasError()) {
+                if (Main.isDebugEnabled()) {
+                    Main.debug("missed tile: " + tile);
+                }
+                missedTiles.add(tile);
+                continue;
+            }
+            Rectangle sourceRect = tileToRect(tile);
+            if (borderRect != null && !sourceRect.intersects(borderRect)) {
+                continue;
+            }
+            drawImageInside(g, img, sourceRect, borderRect);
+        }
+        return missedTiles;
+    }
+
+    private void myDrawString(Graphics g, String text, int x, int y) {
+        Color oldColor = g.getColor();
+        g.setColor(Color.black);
+        g.drawString(text,x+1,y+1);
+        g.setColor(oldColor);
+        g.drawString(text,x,y);
+    }
+
+    private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
+        int fontHeight = g.getFontMetrics().getHeight();
+        if (tile == null)
+            return;
+        Point p = pixelPos(t);
+        int texty = p.y + 2 + fontHeight;
+
+        /*if (PROP_DRAW_DEBUG.get()) {
+            myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
+            texty += 1 + fontHeight;
+            if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
+                myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
+                texty += 1 + fontHeight;
+            }
+        }*/
+
+        /*String tileStatus = tile.getStatus();
+        if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
+            myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
+            texty += 1 + fontHeight;
+        }*/
+
+        if (tile.hasError() && showErrors) {
+            myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
+            texty += 1 + fontHeight;
+        }
+
+        /*int xCursor = -1;
+        int yCursor = -1;
+        if (PROP_DRAW_DEBUG.get()) {
+            if (yCursor < t.getYtile()) {
+                if (t.getYtile() % 32 == 31) {
+                    g.fillRect(0, p.y - 1, mv.getWidth(), 3);
+                } else {
+                    g.drawLine(0, p.y, mv.getWidth(), p.y);
+                }
+                yCursor = t.getYtile();
+            }
+            // This draws the vertical lines for the entire
+            // column. Only draw them for the top tile in
+            // the column.
+            if (xCursor < t.getXtile()) {
+                if (t.getXtile() % 32 == 0) {
+                    // level 7 tile boundary
+                    g.fillRect(p.x - 1, 0, 3, mv.getHeight());
+                } else {
+                    g.drawLine(p.x, 0, p.x, mv.getHeight());
+                }
+                xCursor = t.getXtile();
+            }
+        }*/
+    }
+
+    private Point pixelPos(LatLon ll) {
+        return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
+    }
+
+    private Point pixelPos(Tile t) {
+        double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());
+        LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);
+        return pixelPos(tmpLL);
+    }
+
+    private LatLon getShiftedLatLon(EastNorth en) {
+        return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
+    }
+
+    private Coordinate getShiftedCoord(EastNorth en) {
+        LatLon ll = getShiftedLatLon(en);
+        return new Coordinate(ll.lat(),ll.lon());
+    }
+
+    private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
+    private final class TileSet {
+        int x0, x1, y0, y1;
+        int zoom;
+        int tileMax = -1;
+
+        /**
+         * Create a TileSet by EastNorth bbox taking a layer shift in account
+         */
+        private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
+            this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
+        }
+
+        /**
+         * Create a TileSet by known LatLon bbox without layer shift correction
+         */
+        private TileSet(LatLon topLeft, LatLon botRight, int zoom) {
+            this.zoom = zoom;
+            if (zoom == 0)
+                return;
+
+            x0 = (int)tileSource.lonToTileX(topLeft.lon(),  zoom);
+            y0 = (int)tileSource.latToTileY(topLeft.lat(),  zoom);
+            x1 = (int)tileSource.lonToTileX(botRight.lon(), zoom);
+            y1 = (int)tileSource.latToTileY(botRight.lat(), zoom);
+            if (x0 > x1) {
+                int tmp = x0;
+                x0 = x1;
+                x1 = tmp;
+            }
+            if (y0 > y1) {
+                int tmp = y0;
+                y0 = y1;
+                y1 = tmp;
+            }
+            tileMax = (int)Math.pow(2.0, zoom);
+            if (x0 < 0) {
+                x0 = 0;
+            }
+            if (y0 < 0) {
+                y0 = 0;
+            }
+            if (x1 > tileMax) {
+                x1 = tileMax;
+            }
+            if (y1 > tileMax) {
+                y1 = tileMax;
+            }
+        }
+
+        private boolean tooSmall() {
+            return this.tilesSpanned() < 2.1;
+        }
+
+        private  boolean tooLarge() {
+            return this.tilesSpanned() > 10;
+        }
+
+        private boolean insane() {
+            return this.tilesSpanned() > 100;
+        }
+
+        private double tilesSpanned() {
+            return Math.sqrt(1.0 * this.size());
+        }
+
+        private int size() {
+            int x_span = x1 - x0 + 1;
+            int y_span = y1 - y0 + 1;
+            return x_span * y_span;
+        }
+
+        /*
+         * Get all tiles represented by this TileSet that are
+         * already in the tileCache.
+         */
+        private List<Tile> allExistingTiles() {
+            return this.__allTiles(false);
+        }
+
+        private List<Tile> allTilesCreate() {
+            return this.__allTiles(true);
+        }
+
+        private List<Tile> __allTiles(boolean create) {
+            // Tileset is either empty or too large
+            if (zoom == 0 || this.insane())
+                return Collections.emptyList();
+            List<Tile> ret = new ArrayList<>();
+            for (int x = x0; x <= x1; x++) {
+                for (int y = y0; y <= y1; y++) {
+                    Tile t;
+                    if (create) {
+                        t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
+                    } else {
+                        t = getTile(x % tileMax, y % tileMax, zoom);
+                    }
+                    if (t != null) {
+                        ret.add(t);
+                    }
+                }
+            }
+            return ret;
+        }
+
+        private List<Tile> allLoadedTiles() {
+            List<Tile> ret = new ArrayList<>();
+            for (Tile t : this.allExistingTiles()) {
+                if (t.isLoaded())
+                    ret.add(t);
+            }
+            return ret;
+        }
+
+        /**
+         * @return comparator, that sorts the tiles from the center to the edge of the current screen
+         */
+        private Comparator<Tile> getTileDistanceComparator() {
+            final int centerX = (int) Math.ceil((x0 + x1) / 2d);
+            final int centerY = (int) Math.ceil((y0 + y1) / 2d);
+            return new Comparator<Tile>() {
+                private int getDistance(Tile t) {
+                    return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
+                }
+                @Override
+                public int compare(Tile o1, Tile o2) {
+                    int distance1 = getDistance(o1);
+                    int distance2 = getDistance(o2);
+                    return Integer.compare(distance1, distance2);
+                }
+            };
+        }
+
+
+        private void loadAllTiles(boolean force) {
+            if (!autoLoad && !force)
+                return;
+            List<Tile> allTiles = allTilesCreate();
+            Collections.sort(allTiles, getTileDistanceComparator());
+            for (Tile t : allTiles) {
+                loadTile(t, false);
+            }
+        }
+
+        private void loadAllErrorTiles(boolean force) {
+            if (!autoLoad && !force)
+                return;
+            for (Tile t : this.allTilesCreate()) {
+                if (t.hasError()) {
+                    loadTile(t, true);
+                }
+            }
+        }
+    }
+
+
+    private static class TileSetInfo {
+        public boolean hasVisibleTiles = false;
+        public boolean hasOverzoomedTiles = false;
+        public boolean hasLoadingTiles = false;
+    }
+
+    private static TileSetInfo getTileSetInfo(TileSet ts) {
+        List<Tile> allTiles = ts.allExistingTiles();
+        TileSetInfo result = new TileSetInfo();
+        result.hasLoadingTiles = allTiles.size() < ts.size();
+        for (Tile t : allTiles) {
+            if (t.isLoaded()) {
+                if (!t.hasError()) {
+                    result.hasVisibleTiles = true;
+                }
+                if ("no-tile".equals(t.getValue("tile-info"))) {
+                    result.hasOverzoomedTiles = true;
+                }
+            } else {
+                result.hasLoadingTiles = true;
+            }
+        }
+        return result;
+    }
+
+    private class DeepTileSet {
+        private final EastNorth topLeft, botRight;
+        private final int minZoom, maxZoom;
+        private final TileSet[] tileSets;
+        private final TileSetInfo[] tileSetInfos;
+        public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
+            this.topLeft = topLeft;
+            this.botRight = botRight;
+            this.minZoom = minZoom;
+            this.maxZoom = maxZoom;
+            this.tileSets = new TileSet[maxZoom - minZoom + 1];
+            this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
+        }
+        public TileSet getTileSet(int zoom) {
+            if (zoom < minZoom)
+                return nullTileSet;
+            synchronized (tileSets) {
+                TileSet ts = tileSets[zoom-minZoom];
+                if (ts == null) {
+                    ts = new TileSet(topLeft, botRight, zoom);
+                    tileSets[zoom-minZoom] = ts;
+                }
+                return ts;
+            }
+        }
+
+        public TileSetInfo getTileSetInfo(int zoom) {
+            if (zoom < minZoom)
+                return new TileSetInfo();
+            synchronized (tileSetInfos) {
+                TileSetInfo tsi = tileSetInfos[zoom-minZoom];
+                if (tsi == null) {
+                    tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
+                    tileSetInfos[zoom-minZoom] = tsi;
+                }
+                return tsi;
+            }
+        }
+    }
+
+    @Override
+    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
+
+        if (botRight.east() == 0.0 || botRight.north() == 0) {
+            /*Main.debug("still initializing??");*/
+            // probably still initializing
+            return;
+        }
+
+        needRedraw = false;
+
+        int zoom = currentZoomLevel;
+        if (autoZoom) {
+            double pixelScaling = getScaleFactor(zoom);
+            if (pixelScaling > 3 || pixelScaling < 0.7) {
+                zoom = getBestZoom();
+            }
+        }
+
+        DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
+        TileSet ts = dts.getTileSet(zoom);
+
+        int displayZoomLevel = zoom;
+
+        boolean noTilesAtZoom = false;
+        if (autoZoom && autoLoad) {
+            // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
+            TileSetInfo tsi = dts.getTileSetInfo(zoom);
+            if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
+                noTilesAtZoom = true;
+            }
+            // Find highest zoom level with at least one visible tile
+            for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
+                if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
+                    displayZoomLevel = tmpZoom;
+                    break;
+                }
+            }
+            // Do binary search between currentZoomLevel and displayZoomLevel
+            while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
+                zoom = (zoom + displayZoomLevel)/2;
+                tsi = dts.getTileSetInfo(zoom);
+            }
+
+            setZoomLevel(zoom);
+
+            // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
+            // to make sure there're really no more zoom levels
+            if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
+                zoom++;
+                tsi = dts.getTileSetInfo(zoom);
+            }
+            // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
+            // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
+            while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
+                zoom--;
+                tsi = dts.getTileSetInfo(zoom);
+            }
+            ts = dts.getTileSet(zoom);
+        } else if (autoZoom) {
+            setZoomLevel(zoom);
+        }
+
+        // Too many tiles... refuse to download
+        if (!ts.tooLarge()) {
+            //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
+            ts.loadAllTiles(false);
+        }
+
+        if (displayZoomLevel != zoom) {
+            ts = dts.getTileSet(displayZoomLevel);
+        }
+
+        g.setColor(Color.DARK_GRAY);
+
+        List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
+        int[] otherZooms = { -1, 1, -2, 2, -3, -4, -5};
+        for (int zoomOffset : otherZooms) {
+            if (!autoZoom) {
+                break;
+            }
+            int newzoom = displayZoomLevel + zoomOffset;
+            if (newzoom < MIN_ZOOM) {
+                continue;
+            }
+            if (missedTiles.isEmpty()) {
+                break;
+            }
+            List<Tile> newlyMissedTiles = new LinkedList<>();
+            for (Tile missed : missedTiles) {
+                if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
+                    // Don't try to paint from higher zoom levels when tile is overzoomed
+                    newlyMissedTiles.add(missed);
+                    continue;
+                }
+                Tile t2 = tempCornerTile(missed);
+                LatLon topLeft2  = tileLatLon(missed);
+                LatLon botRight2 = tileLatLon(t2);
+                TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
+                // Instantiating large TileSets is expensive.  If there
+                // are no loaded tiles, don't bother even trying.
+                if (ts2.allLoadedTiles().isEmpty()) {
+                    newlyMissedTiles.add(missed);
+                    continue;
+                }
+                if (ts2.tooLarge()) {
+                    continue;
+                }
+                newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
+            }
+            missedTiles = newlyMissedTiles;
+        }
+        if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
+            Main.debug("still missed "+missedTiles.size()+" in the end");
+        }
+        g.setColor(Color.red);
+        g.setFont(InfoFont);
+
+        // The current zoom tileset should have all of its tiles
+        // due to the loadAllTiles(), unless it to tooLarge()
+        for (Tile t : ts.allExistingTiles()) {
+            this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
+        }
+
+        attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this);
+
+        //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
+        g.setColor(Color.lightGray);
+        if (!autoZoom) {
+            if (ts.insane()) {
+                myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
+            } else if (ts.tooLarge()) {
+                myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
+            } else if (ts.tooSmall()) {
+                myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
+            }
+        }
+        if (noTilesAtZoom) {
+            myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
+        }
+        if (Main.isDebugEnabled()) {
+            myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
+            myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
+            myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
+            myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
+            if(tileLoader instanceof TMSCachedTileLoader) {
+                TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;
+                int offset = 185;
+                for(String part: cachedTileLoader.getStats().split("\n")) {
+                    myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);
+                }
+
+            }
+        }
+    }
+
+    /**
+     * This isn't very efficient, but it is only used when the
+     * user right-clicks on the map.
+     */
+    private Tile getTileForPixelpos(int px, int py) {
+        if (Main.isDebugEnabled()) {
+            Main.debug("getTileForPixelpos("+px+", "+py+")");
+        }
+        MapView mv = Main.map.mapView;
+        Point clicked = new Point(px, py);
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
+        int z = currentZoomLevel;
+        TileSet ts = new TileSet(topLeft, botRight, z);
+
+        if (!ts.tooLarge()) {
+            ts.loadAllTiles(false); // make sure there are tile objects for all tiles
+        }
+        Tile clickedTile = null;
+        for (Tile t1 : ts.allExistingTiles()) {
+            Tile t2 = tempCornerTile(t1);
+            Rectangle r = new Rectangle(pixelPos(t1));
+            r.add(pixelPos(t2));
+            if (Main.isDebugEnabled()) {
+                Main.debug("r: " + r + " clicked: " + clicked);
+            }
+            if (!r.contains(clicked)) {
+                continue;
+            }
+            clickedTile  = t1;
+            break;
+        }
+        if (clickedTile == null)
+            return null;
+        /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
+                " currentZoomLevel: " + currentZoomLevel);*/
+        return clickedTile;
+    }
+
+    @Override
+    public Action[] getMenuEntries() {
+        return new Action[] {
+                LayerListDialog.getInstance().createActivateLayerAction(this),
+                LayerListDialog.getInstance().createShowHideLayerAction(),
+                LayerListDialog.getInstance().createDeleteLayerAction(),
+                SeparatorLayerAction.INSTANCE,
+                // color,
+                new OffsetAction(),
+                new RenameLayerAction(this.getAssociatedFile(), this),
+                SeparatorLayerAction.INSTANCE,
+                new AutoLoadTilesAction(),
+                new AutoZoomAction(),
+                new ZoomToBestAction(),
+                new ZoomToNativeLevelAction(),
+                new LoadErroneusTilesAction(),
+                new LoadAllTilesAction(),
+                new LayerListPopup.InfoAction(this)
+        };
+    }
+
+    @Override
+    public String getToolTipText() {
+        if(autoLoad) {
+            return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
+        } else {
+            return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
+        }
+    }
+
+    @Override
+    public void visitBoundingBox(BoundingXYVisitor v) {
+    }
+
+    @Override
+    public boolean isChanged() {
+        return needRedraw;
+    }
+
+    /**
+     * Task responsible for precaching imagery along the gpx track
+     *
+     */
+    public class PrecacheTask implements TileLoaderListener {
+        private final ProgressMonitor progressMonitor;
+        private volatile int totalCount;
+        private volatile int processedCount = 0;
+        private TileLoader tileLoader;
+
+        /**
+         * @param progressMonitor that will be notified about progess of the task
+         */
+        public PrecacheTask(ProgressMonitor progressMonitor) {
+            this.progressMonitor = progressMonitor;
+            this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
+            if (this.tileLoader instanceof TMSCachedTileLoader) {
+                ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
+                        TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
+            }
+
+        }
+
+        /**
+         * @return true, if all is done
+         */
+        public boolean isFinished() {
+            return processedCount >= totalCount;
+        }
+
+        /**
+         * @return total number of tiles to download
+         */
+        public int getTotalCount() {
+            return totalCount;
+        }
+
+        /**
+         * cancel the task
+         */
+        public void cancel() {
+            if (tileLoader instanceof TMSCachedTileLoader) {
+                ((TMSCachedTileLoader)tileLoader).cancelOutstandingTasks();
+            }
+        }
+
+
+        @Override
+        public void tileLoadingFinished(Tile tile, boolean success) {
+            if (success) {
+                this.processedCount++;
+                this.progressMonitor.worked(1);
+                this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processedCount, totalCount));
+            }
+        }
+
+        /**
+         * @return tile loader that is used to load the tiles
+         */
+        public TileLoader getTileLoader() {
+            return tileLoader;
+        }
+    }
+
+    /**
+     * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
+     * all of the tiles. Buffer contains at least one tile.
+     *
+     * To prevent accidental clear of the queue, new download executor is created with separate queue
+     *
+     * @param precacheTask
+     * @param points
+     * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
+     * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
+     */
+    public void downloadAreaToCache(final PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
+        final Set<Tile> requestedTiles = new ConcurrentSkipListSet <>(new Comparator<Tile>() {
+            public int compare(Tile o1, Tile o2) {
+                return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey());
+            }
+        });
+        for (LatLon point: points) {
+            int minY = (int) Math.round(tileSource.latToTileY(point.lat() - bufferY, currentZoomLevel));
+            int curY = (int) Math.round(tileSource.latToTileY(point.lat(), currentZoomLevel));
+            int maxY = (int) Math.round(tileSource.latToTileY(point.lat() + bufferY, currentZoomLevel));
+            int minX = (int) Math.round(tileSource.lonToTileX(point.lon() - bufferX, currentZoomLevel));
+            int curX = (int) Math.round(tileSource.lonToTileX(point.lon(), currentZoomLevel));
+            int maxX = (int) Math.round(tileSource.lonToTileX(point.lon() + bufferX, currentZoomLevel));
+
+            // take at least one tile of buffer
+            minY = Math.min(curY - 1, minY);
+            maxY = Math.max(curY + 1, maxY);
+            minX = Math.min(curX - 1, minX);
+            maxX = Math.min(curX + 1, maxX);
+
+            for (int x= minX; x<=maxX; x++) {
+                for (int y= minY; y<=maxY; y++) {
+                    requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
+                }
+            }
+        }
+
+        precacheTask.totalCount = requestedTiles.size();
+        precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
+
+        TileLoader loader = precacheTask.getTileLoader();
+        for (Tile t: requestedTiles) {
+            loader.createTileLoaderJob(t).submit();
+        }
+    }
+
+    @Override
+    public boolean isSavable() {
+        return true; // With WMSLayerExporter
+    }
+
+    @Override
+    public File createAndOpenSaveFileChooser() {
+        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/layer/TMSLayer.java b/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
index 9201712..a539567 100644
--- a/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
@@ -3,94 +3,23 @@ package org.openstreetmap.josm.gui.layer;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
-import java.awt.Color;
-import java.awt.Font;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.GridBagLayout;
-import java.awt.Image;
-import java.awt.Point;
-import java.awt.Rectangle;
-import java.awt.Toolkit;
-import java.awt.event.ActionEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.image.ImageObserver;
-import java.io.File;
 import java.io.IOException;
-import java.io.StringReader;
-import java.net.URL;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Scanner;
-import java.util.concurrent.Callable;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
-import javax.swing.AbstractAction;
-import javax.swing.Action;
-import javax.swing.BorderFactory;
-import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JLabel;
-import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JPopupMenu;
-import javax.swing.JTextField;
-
-import org.openstreetmap.gui.jmapviewer.AttributionSupport;
-import org.openstreetmap.gui.jmapviewer.Coordinate;
-import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
-import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
-import org.openstreetmap.gui.jmapviewer.Tile;
-import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
-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.BingAerialTileSource;
-import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
-import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
 import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.RenameLayerAction;
-import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.Version;
-import org.openstreetmap.josm.data.coor.EastNorth;
-import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
-import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
-import org.openstreetmap.josm.data.preferences.StringProperty;
 import org.openstreetmap.josm.data.projection.Projection;
-import org.openstreetmap.josm.gui.ExtendedDialog;
-import org.openstreetmap.josm.gui.MapFrame;
-import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
-import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
-import org.openstreetmap.josm.gui.PleaseWaitRunnable;
-import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
-import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
-import org.openstreetmap.josm.gui.progress.ProgressMonitor;
-import org.openstreetmap.josm.io.CacheCustomContent;
-import org.openstreetmap.josm.io.OsmTransferException;
-import org.openstreetmap.josm.io.UTFInputStreamReader;
-import org.openstreetmap.josm.tools.CheckParameterUtil;
-import org.openstreetmap.josm.tools.GBC;
-import org.openstreetmap.josm.tools.Utils;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
+
 
 /**
  * Class that displays a slippy map layer.
@@ -101,250 +30,55 @@ import org.xml.sax.SAXException;
  * @author Upliner &lt;upliner@gmail.com&gt;
  *
  */
-public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
-    public static final String PREFERENCE_PREFIX   = "imagery.tms";
-
-    public static final int MAX_ZOOM = 30;
-    public static final int MIN_ZOOM = 2;
-    public static final int DEFAULT_MAX_ZOOM = 20;
-    public static final int DEFAULT_MIN_ZOOM = 2;
-
-    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
-    public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
-    public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true);
-    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM);
-    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
-    public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX +
-            ".add_to_slippymap_chooser", true);
-    public static final StringProperty PROP_TILECACHE_DIR;
+public class TMSLayer extends AbstractTileSourceLayer {
+    private static final String PREFERENCE_PREFIX   = "imagery.tms";
 
-    static {
-        String defPath = null;
-        try {
-            defPath = new File(Main.pref.getCacheDirectory(), "tms").getAbsolutePath();
-        } catch (SecurityException e) {
-            Main.warn(e);
-        }
-        PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache", defPath);
-    }
+    /** minimum zoom level for TMS layer */
+    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", AbstractTileSourceLayer.PROP_MIN_ZOOM_LVL.get());
+    /** maximum zoom level for TMS layer */
+    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", AbstractTileSourceLayer.PROP_MAX_ZOOM_LVL.get());
+    /** shall TMS layers be added to download dialog */
+    public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
 
-    /**
-     * Interface for creating TileLoaders, ie. classes responsible for loading tiles on map
-     *
-     */
-    public interface TileLoaderFactory {
-        /**
-         * @param listener object that will be notified, when tile has finished loading
-         * @return TileLoader that will notify the listener
-         */
-        TileLoader makeTileLoader(TileLoaderListener listener);
-
-        /**
-         * @param listener object that will be notified, when tile has finished loading
-         * @param headers HTTP headers that should be sent by TileLoader to tile server
-         * @return TileLoader that will notify the listener
-         */
-        TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
-    }
+    /** loader factory responsible for loading tiles for this layer */
+    public static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("TMS"){
 
-    protected TileCache tileCache;
-    protected TileSource tileSource;
-    protected TileLoader tileLoader;
-
-
-    public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
         @Override
-        public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
-            Map<String, String> headers = new HashMap<>();
-            headers.put("User-Agent", Version.getInstance().getFullAgentString());
-            headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
-            if (inputHeaders != null)
-                headers.putAll(inputHeaders);
-
-            try {
-                return new TMSCachedTileLoader(listener, "TMS",
-                        Main.pref.getInteger("socket.timeout.connect",15) * 1000,
-                        Main.pref.getInteger("socket.timeout.read", 30) * 1000,
-                        headers,
-                        PROP_TILECACHE_DIR.get());
-            } catch (IOException e) {
-                Main.warn(e);
-            }
-            return null;
+        protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout,
+                int readTimeout, Map<String, String> headers, String cacheDir) throws IOException {
+            return new TMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir);
         }
 
-        @Override
-        public TileLoader makeTileLoader(TileLoaderListener listener) {
-            return makeTileLoader(listener, null);
-        }
     };
 
     /**
-     * Plugins that wish to set custom tile loader should call this method
+     * Create a layer based on ImageryInfo
+     * @param info description of the layer
      */
-
-    public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
-        TMSLayer.loaderFactory = loaderFactory;
-    }
-
-    @Override
-    public synchronized void tileLoadingFinished(Tile tile, boolean success) {
-        if (tile.hasError()) {
-            success = false;
-            tile.setImage(null);
-        }
-        if (sharpenLevel != 0 && success) {
-            tile.setImage(sharpenImage(tile.getImage()));
-        }
-        tile.setLoaded(success);
-        needRedraw = true;
-        if (Main.map != null) {
-            Main.map.repaint(100);
-        }
-        if (Main.isDebugEnabled()) {
-            Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
-        }
-    }
-
-    /**
-     * Clears the tile cache.
-     *
-     * If the current tileLoader is an instance of OsmTileLoader, a new
-     * TmsTileClearController is created and passed to the according clearCache
-     * method.
-     *
-     * @param monitor not used in this implementation - as cache clear is instaneus
-     */
-    public void clearTileCache(ProgressMonitor monitor) {
-        tileCache.clear();
-        if (tileLoader instanceof CachedTileLoader) {
-            ((CachedTileLoader)tileLoader).clearCache(tileSource);
-        }
-        redraw();
+    public TMSLayer(ImageryInfo info) {
+        super(info);
     }
 
     /**
-     * Zoomlevel at which tiles is currently downloaded.
-     * Initial zoom lvl is set to bestZoom
-     */
-    public int currentZoomLevel;
-
-    private Tile clickedTile;
-    private boolean needRedraw;
-    private JPopupMenu tileOptionMenu;
-    private JCheckBoxMenuItem autoZoomPopup;
-    private JCheckBoxMenuItem autoLoadPopup;
-    private JCheckBoxMenuItem showErrorsPopup;
-    private Tile showMetadataTile;
-    private AttributionSupport attribution = new AttributionSupport();
-    private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
-
-    protected boolean autoZoom;
-    protected boolean autoLoad;
-    protected boolean showErrors;
-
-    /**
-     * Initiates a repaint of Main.map
-     *
-     * @see Main#map
-     * @see MapFrame#repaint()
+     * Plugins that wish to set custom tile loader should call this method
+     * @param newLoaderFactory that will be used to load tiles
      */
-    protected void redraw() {
-        needRedraw = true;
-        Main.map.repaint();
-    }
-
-    protected static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
-        if(maxZoomLvl > MAX_ZOOM) {
-            maxZoomLvl = MAX_ZOOM;
-        }
-        if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
-            maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
-        }
-        if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
-            maxZoomLvl = ts.getMaxZoom();
-        }
-        return maxZoomLvl;
-    }
-
-    public static int getMaxZoomLvl(TileSource ts) {
-        return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
-    }
-
-    public static void setMaxZoomLvl(int maxZoomLvl) {
-        Integer newMaxZoom = Integer.valueOf(checkMaxZoomLvl(maxZoomLvl, null));
-        PROP_MAX_ZOOM_LVL.put(newMaxZoom);
-    }
 
-    static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
-        if(minZoomLvl < MIN_ZOOM) {
-            /*Main.debug("Min. zoom level should not be less than "+MIN_ZOOM+"! Setting to that.");*/
-            minZoomLvl = MIN_ZOOM;
-        }
-        if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
-            /*Main.debug("Min. zoom level should not be more than Max. zoom level! Setting to Max.");*/
-            minZoomLvl = getMaxZoomLvl(ts);
-        }
-        if (ts != null && ts.getMinZoom() > minZoomLvl) {
-            /*Main.debug("Increasing min. zoom level to match tile source");*/
-            minZoomLvl = ts.getMinZoom();
-        }
-        return minZoomLvl;
-    }
-
-    public static int getMinZoomLvl(TileSource ts) {
-        return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
-    }
-
-    public static void setMinZoomLvl(int minZoomLvl) {
-        minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
-        PROP_MIN_ZOOM_LVL.put(minZoomLvl);
-    }
-
-    private static class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
-
-        public CachedAttributionBingAerialTileSource(ImageryInfo info) {
-            super(info);
-        }
-
-        class BingAttributionData extends CacheCustomContent<IOException> {
-
-            public BingAttributionData() {
-                super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
+    public static void setTileLoaderFactory(TileLoaderFactory newLoaderFactory) {
+        loaderFactory = newLoaderFactory;
     }
 
     @Override
-            protected byte[] updateData() throws IOException {
-                URL u = getAttributionUrl();
-                try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) {
-                    String r = scanner.useDelimiter("\\A").next();
-                    Main.info("Successfully loaded Bing attribution data.");
-                    return r.getBytes("UTF-8");
-                }
+    protected TileLoaderFactory getTileLoaderFactory() {
+        return loaderFactory;
     }
-        }
-
-        @Override
-        protected Callable<List<Attribution>> getAttributionLoaderCallable() {
-            return new Callable<List<Attribution>>() {
 
     @Override
-                public List<Attribution> call() throws Exception {
-                    BingAttributionData attributionLoader = new BingAttributionData();
-                    int waitTimeSec = 1;
-                    while (true) {
-                        try {
-                            String xml = attributionLoader.updateIfRequiredString();
-                            return parseAttributionText(new InputSource(new StringReader(xml)));
-                        } catch (IOException ex) {
-                            Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
-                            Thread.sleep(waitTimeSec * 1000L);
-                            waitTimeSec *= 2;
-                        }
-                    }
-                }
-            };
+    protected Map<String, String> getHeaders(TileSource tileSource) {
+        if (tileSource instanceof TemplatedTMSTileSource) {
+            return ((TemplatedTMSTileSource)tileSource).getHeaders();
         }
+        return null;
     }
 
     /**
@@ -359,1210 +93,15 @@ public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderL
      * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.
      * @throws IllegalArgumentException
      */
-    public static TileSource getTileSource(ImageryInfo info) {
-        if (info.getImageryType() == ImageryType.TMS) {
-            checkUrl(info.getUrl());
-            TMSTileSource t = new TemplatedTMSTileSource(info);
-            info.setAttribution(t);
-            return t;
-        } else if (info.getImageryType() == ImageryType.BING) {
-            return new CachedAttributionBingAerialTileSource(info);
-        } else if (info.getImageryType() == ImageryType.SCANEX) {
-            return new ScanexTileSource(info);
-        }
-        return null;
-    }
-
-    /**
-     * Checks validity of given URL.
-     * @param url URL to check
-     * @throws IllegalArgumentException if url is null or invalid
-     */
-    public static void checkUrl(String url) {
-        CheckParameterUtil.ensureParameterNotNull(url, "url");
-        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
-        while (m.find()) {
-            boolean isSupportedPattern = false;
-            for (String pattern : TemplatedTMSTileSource.ALL_PATTERNS) {
-                if (m.group().matches(pattern)) {
-                    isSupportedPattern = true;
-                    break;
-                }
-            }
-            if (!isSupportedPattern) {
-                throw new IllegalArgumentException(
-                        tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
-            }
-        }
-    }
-
-    private void initTileSource(TileSource tileSource) {
-        this.tileSource = tileSource;
-        attribution.initialize(tileSource);
-
-        currentZoomLevel = getBestZoom();
-
-        Map<String, String> headers = null;
-        if (tileSource instanceof TemplatedTMSTileSource) {
-            headers = (((TemplatedTMSTileSource)tileSource).getHeaders());
-        }
-
-        tileLoader = loaderFactory.makeTileLoader(this, headers);
-        if (tileLoader instanceof TMSCachedTileLoader) {
-            tileCache = (TileCache) tileLoader;
-        } else {
-            tileCache = new MemoryTileCache();
-        }
-        if (tileLoader == null)
-            tileLoader = new OsmTileLoader(this);
-    }
-
-    /**
-     * Marks layer as needing redraw on offset change
-     */
     @Override
-    public void setOffset(double dx, double dy) {
-        super.setOffset(dx, dy);
-        needRedraw = true;
-    }
-    /**
-     * Returns average number of screen pixels per tile pixel for current mapview
-     */
-    private double getScaleFactor(int zoom) {
-        if (!Main.isDisplayingMapView()) return 1;
-        MapView mv = Main.map.mapView;
-        LatLon topLeft = mv.getLatLon(0, 0);
-        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
-        double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
-        double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
-        double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
-        double y2 = tileSource.latToTileY(botRight.lat(), zoom);
-
-        int screenPixels = mv.getWidth()*mv.getHeight();
-        double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
-        if (screenPixels == 0 || tilePixels == 0) return 1;
-        return screenPixels/tilePixels;
-    }
-
-    private final int getBestZoom() {
-        double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
-        double result = Math.log(factor)/Math.log(2)/2+1;
-        /*
-         * Math.log(factor)/Math.log(2) - gives log base 2 of factor
-         * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
-         * In general, smaller zoom levels are more readable.  We prefer big,
-         * block, pixelated (but readable) map text to small, smeared,
-         * unreadable underzoomed text.  So, use .floor() instead of rounding
-         * to skew things a bit toward the lower zooms.
-         * Remember, that result here, should correspond to TMSLayer.paint(...)
-         * getScaleFactor(...) is supposed to be between 0.75 and 3
-         */
-        int intResult = (int)Math.floor(result);
-        if (intResult > getMaxZoomLvl())
-            return getMaxZoomLvl();
-        if (intResult < getMinZoomLvl())
-            return getMinZoomLvl();
-        return intResult;
-    }
-
-    @SuppressWarnings("serial")
-    public TMSLayer(ImageryInfo info) {
-        super(info);
-
-        if(!isProjectionSupported(Main.getProjection())) {
-            JOptionPane.showMessageDialog(Main.parent,
-                    tr("TMS layers do not support the projection {0}.\n{1}\n"
-                            + "Change the projection or remove the layer.",
-                            Main.getProjection().toCode(), nameSupportedProjections()),
-                            tr("Warning"),
-                            JOptionPane.WARNING_MESSAGE);
+    protected TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
+        return AbstractTMSTileSource.getTileSource(info);
     }
 
-        setBackgroundLayer(true);
-        this.setVisible(true);
-
-        TileSource source = getTileSource(info);
-        if (source == null)
-            throw new IllegalStateException("Cannot create TMSLayer with non-TMS ImageryInfo");
-        initTileSource(source);
-
-        MapView.addZoomChangeListener(this);
-    }
 
     /**
      * Adds a context menu to the mapView.
      */
-    @Override
-    public void hookUpMapView() {
-        tileOptionMenu = new JPopupMenu();
-
-        autoZoom = PROP_DEFAULT_AUTOZOOM.get();
-        autoZoomPopup = new JCheckBoxMenuItem();
-        autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                autoZoom = !autoZoom;
-            }
-        });
-        autoZoomPopup.setSelected(autoZoom);
-        tileOptionMenu.add(autoZoomPopup);
-
-        autoLoad = PROP_DEFAULT_AUTOLOAD.get();
-        autoLoadPopup = new JCheckBoxMenuItem();
-        autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                autoLoad= !autoLoad;
-            }
-        });
-        autoLoadPopup.setSelected(autoLoad);
-        tileOptionMenu.add(autoLoadPopup);
-
-        showErrors = PROP_DEFAULT_SHOWERRORS.get();
-        showErrorsPopup = new JCheckBoxMenuItem();
-        showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                showErrors = !showErrors;
-            }
-        });
-        showErrorsPopup.setSelected(showErrors);
-        tileOptionMenu.add(showErrorsPopup);
-
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                if (clickedTile != null) {
-                    loadTile(clickedTile, true);
-                    redraw();
-                }
-            }
-        }));
-
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(
-                tr("Show Tile Info")) {
-            private String getSizeString(int size) {
-                StringBuilder ret = new StringBuilder();
-                return ret.append(size).append("x").append(size).toString();
-            }
-
-            private JTextField createTextField(String text) {
-                JTextField ret = new JTextField(text);
-                ret.setEditable(false);
-                ret.setBorder(BorderFactory.createEmptyBorder());
-                return ret;
-            }
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                if (clickedTile != null) {
-                    ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
-                    JPanel panel = new JPanel(new GridBagLayout());
-                    Rectangle displaySize = tileToRect(clickedTile);
-                    String url = "";
-                    try {
-                        url = clickedTile.getUrl();
-                    } catch (IOException e) {
-                        // silence exceptions
-                    }
-
-                    String[][] content = {
-                            {"Tile name", clickedTile.getKey()},
-                            {"Tile url", url},
-                            {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
-                            {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
-                    };
-
-                    for (String[] entry: content) {
-                        panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
-                        panel.add(GBC.glue(5,0), GBC.std());
-                        panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
-                    }
-
-                    for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
-                        panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
-                        panel.add(GBC.glue(5,0), GBC.std());
-                        String value = e.getValue();
-                        if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
-                            value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
-                        }
-                        panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
-
-                    }
-                    ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
-                    ed.setContent(panel);
-                    ed.showDialog();
-                }
-            }
-        }));
-
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(
-                tr("Request Update")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                if (clickedTile != null) {
-                    clickedTile.setLoaded(false);
-                    tileLoader.createTileLoaderJob(clickedTile).submit();
-                }
-            }
-        }));
-
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(
-                tr("Load All Tiles")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                loadAllTiles(true);
-                redraw();
-            }
-        }));
-
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(
-                tr("Load All Error Tiles")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                loadAllErrorTiles(true);
-                redraw();
-            }
-        }));
-
-        // increase and decrease commands
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(
-                tr("Increase zoom")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                increaseZoomLevel();
-                redraw();
-            }
-        }));
-
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(
-                tr("Decrease zoom")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                decreaseZoomLevel();
-                redraw();
-            }
-        }));
-
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(
-                tr("Snap to tile size")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
-                Main.map.mapView.zoomToFactor(newFactor);
-                redraw();
-            }
-        }));
-
-        tileOptionMenu.add(new JMenuItem(new AbstractAction(
-                tr("Flush Tile Cache")) {
-            @Override
-            public void actionPerformed(ActionEvent ae) {
-                new PleaseWaitRunnable(tr("Flush Tile Cache")) {
-                    @Override
-                    protected void realRun() throws SAXException, IOException,
-                            OsmTransferException {
-                        clearTileCache(getProgressMonitor());
-                    }
-
-                    @Override
-                    protected void finish() {
-                    }
-
-                    @Override
-                    protected void cancel() {
-                    }
-                }.run();
-            }
-        }));
-
-        final MouseAdapter adapter = new MouseAdapter() {
-            @Override
-            public void mouseClicked(MouseEvent e) {
-                if (!isVisible()) return;
-                if (e.getButton() == MouseEvent.BUTTON3) {
-                    clickedTile = getTileForPixelpos(e.getX(), e.getY());
-                    tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
-                } else if (e.getButton() == MouseEvent.BUTTON1) {
-                    attribution.handleAttribution(e.getPoint(), true);
-                }
-            }
-        };
-        Main.map.mapView.addMouseListener(adapter);
-
-        MapView.addLayerChangeListener(new LayerChangeListener() {
-            @Override
-            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
-                //
-            }
-
-            @Override
-            public void layerAdded(Layer newLayer) {
-                //
-            }
-
-            @Override
-            public void layerRemoved(Layer oldLayer) {
-                if (oldLayer == TMSLayer.this) {
-                    Main.map.mapView.removeMouseListener(adapter);
-                    MapView.removeZoomChangeListener(TMSLayer.this);
-                    MapView.removeLayerChangeListener(this);
-                }
-            }
-        });
-    }
-
-    /**
-     * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
-     * changes to visible map (panning/zooming)
-     */
-    @Override
-    public void zoomChanged() {
-        if (Main.isDebugEnabled()) {
-            Main.debug("zoomChanged(): " + currentZoomLevel);
-        }
-        needRedraw = true;
-        if (tileLoader instanceof TMSCachedTileLoader) {
-            ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
-        }
-    }
-
-    protected int getMaxZoomLvl() {
-        if (info.getMaxZoom() != 0)
-            return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
-        else
-            return getMaxZoomLvl(tileSource);
-    }
-
-    protected int getMinZoomLvl() {
-        return getMinZoomLvl(tileSource);
-    }
-
-    /**
-     * Zoom in, go closer to map.
-     *
-     * @return    true, if zoom increasing was successful, false otherwise
-     */
-    public boolean zoomIncreaseAllowed() {
-        boolean zia = currentZoomLevel < this.getMaxZoomLvl();
-        if (Main.isDebugEnabled()) {
-            Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
-        }
-        return zia;
-    }
-
-    public boolean increaseZoomLevel() {
-        if (zoomIncreaseAllowed()) {
-            currentZoomLevel++;
-            if (Main.isDebugEnabled()) {
-                Main.debug("increasing zoom level to: " + currentZoomLevel);
-            }
-            zoomChanged();
-        } else {
-            Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
-                    "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
-            return false;
-        }
-        return true;
-    }
-
-    public boolean setZoomLevel(int zoom) {
-        if (zoom == currentZoomLevel) return true;
-        if (zoom > this.getMaxZoomLvl()) return false;
-        if (zoom < this.getMinZoomLvl()) return false;
-        currentZoomLevel = zoom;
-        zoomChanged();
-        return true;
-    }
-
-    /**
-     * Check if zooming out is allowed
-     *
-     * @return    true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
-     */
-    public boolean zoomDecreaseAllowed() {
-        return currentZoomLevel > this.getMinZoomLvl();
-    }
-
-    /**
-     * Zoom out from map.
-     *
-     * @return    true, if zoom increasing was successfull, false othervise
-     */
-    public boolean decreaseZoomLevel() {
-        //int minZoom = this.getMinZoomLvl();
-        if (zoomDecreaseAllowed()) {
-            if (Main.isDebugEnabled()) {
-                Main.debug("decreasing zoom level to: " + currentZoomLevel);
-            }
-            currentZoomLevel--;
-            zoomChanged();
-        } else {
-            /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/
-            return false;
-        }
-        return true;
-    }
-
-    /*
-     * We use these for quick, hackish calculations.  They
-     * are temporary only and intentionally not inserted
-     * into the tileCache.
-     */
-    private Tile tempCornerTile(Tile t) {
-        int x = t.getXtile() + 1;
-        int y = t.getYtile() + 1;
-        int zoom = t.getZoom();
-        Tile tile = getTile(x, y, zoom);
-        if (tile != null)
-            return tile;
-        return new Tile(tileSource, x, y, zoom);
-    }
-
-    private Tile getOrCreateTile(int x, int y, int zoom) {
-        Tile tile = getTile(x, y, zoom);
-        if (tile == null) {
-            tile = new Tile(tileSource, x, y, zoom);
-            tileCache.addTile(tile);
-            tile.loadPlaceholderFromCache(tileCache);
-        }
-        return tile;
-    }
-
-    /*
-     * This can and will return null for tiles that are not
-     * already in the cache.
-     */
-    private Tile getTile(int x, int y, int zoom) {
-        int max = 1 << zoom;
-        if (x < 0 || x >= max || y < 0 || y >= max)
-            return null;
-        return tileCache.getTile(tileSource, x, y, zoom);
-    }
-
-    private boolean loadTile(Tile tile, boolean force) {
-        if (tile == null)
-            return false;
-        if (!force && (tile.isLoaded() || tile.hasError()))
-            return false;
-        if (tile.isLoading())
-            return false;
-        tileLoader.createTileLoaderJob(tile).submit();
-        return true;
-    }
-
-    private void loadAllTiles(boolean force) {
-        MapView mv = Main.map.mapView;
-        EastNorth topLeft = mv.getEastNorth(0, 0);
-        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
-
-        TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
-
-        // if there is more than 18 tiles on screen in any direction, do not
-        // load all tiles!
-        if (ts.tooLarge()) {
-            Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
-            return;
-        }
-        ts.loadAllTiles(force);
-    }
-
-    private void loadAllErrorTiles(boolean force) {
-        MapView mv = Main.map.mapView;
-        EastNorth topLeft = mv.getEastNorth(0, 0);
-        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
-
-        TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
-
-        ts.loadAllErrorTiles(force);
-    }
-
-    @Override
-    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
-        boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
-        needRedraw = true;
-        if (Main.isDebugEnabled()) {
-            Main.debug("imageUpdate() done: " + done + " calling repaint");
-        }
-        Main.map.repaint(done ? 0 : 100);
-        return !done;
-    }
-
-    private boolean imageLoaded(Image i) {
-        if (i == null)
-            return false;
-        int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
-        if ((status & ALLBITS) != 0)
-            return true;
-        return false;
-    }
-
-    /**
-     * Returns the image for the given tile if both tile and image are loaded.
-     * Otherwise returns  null.
-     *
-     * @param tile the Tile for which the image should be returned
-     * @return  the image of the tile or null.
-     */
-    private Image getLoadedTileImage(Tile tile) {
-        if (!tile.isLoaded())
-            return null;
-        Image img = tile.getImage();
-        if (!imageLoaded(img))
-            return null;
-        return img;
-    }
-
-    private LatLon tileLatLon(Tile t) {
-        int zoom = t.getZoom();
-        return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),
-                tileSource.tileXToLon(t.getXtile(), zoom));
-    }
-
-    private Rectangle tileToRect(Tile t1) {
-        /*
-         * We need to get a box in which to draw, so advance by one tile in
-         * each direction to find the other corner of the box.
-         * Note: this somewhat pollutes the tile cache
-         */
-        Tile t2 = tempCornerTile(t1);
-        Rectangle rect = new Rectangle(pixelPos(t1));
-        rect.add(pixelPos(t2));
-        return rect;
-    }
-
-    // 'source' is the pixel coordinates for the area that
-    // the img is capable of filling in.  However, we probably
-    // only want a portion of it.
-    //
-    // 'border' is the screen cordinates that need to be drawn.
-    //  We must not draw outside of it.
-    private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
-        Rectangle target = source;
-
-        // If a border is specified, only draw the intersection
-        // if what we have combined with what we are supposed
-        // to draw.
-        if (border != null) {
-            target = source.intersection(border);
-            if (Main.isDebugEnabled()) {
-                Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
-            }
-        }
-
-        // All of the rectangles are in screen coordinates.  We need
-        // to how these correlate to the sourceImg pixels.  We could
-        // avoid doing this by scaling the image up to the 'source' size,
-        // but this should be cheaper.
-        //
-        // In some projections, x any y are scaled differently enough to
-        // cause a pixel or two of fudge.  Calculate them separately.
-        double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
-        double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
-
-        // How many pixels into the 'source' rectangle are we drawing?
-        int screen_x_offset = target.x - source.x;
-        int screen_y_offset = target.y - source.y;
-        // And how many pixels into the image itself does that
-        // correlate to?
-        int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5);
-        int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5);
-        // Now calculate the other corner of the image that we need
-        // by scaling the 'target' rectangle's dimensions.
-        int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5);
-        int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5);
-
-        if (Main.isDebugEnabled()) {
-            Main.debug("drawing image into target rect: " + target);
-        }
-        g.drawImage(sourceImg,
-                target.x, target.y,
-                target.x + target.width, target.y + target.height,
-                img_x_offset, img_y_offset,
-                img_x_end, img_y_end,
-                this);
-        if (PROP_FADE_AMOUNT.get() != 0) {
-            // dimm by painting opaque rect...
-            g.setColor(getFadeColorWithAlpha());
-            g.fillRect(target.x, target.y,
-                    target.width, target.height);
-        }
-    }
-
-    // This function is called for several zoom levels, not just
-    // the current one.  It should not trigger any tiles to be
-    // downloaded.  It should also avoid polluting the tile cache
-    // with any tiles since these tiles are not mandatory.
-    //
-    // The "border" tile tells us the boundaries of where we may
-    // draw.  It will not be from the zoom level that is being
-    // drawn currently.  If drawing the displayZoomLevel,
-    // border is null and we draw the entire tile set.
-    private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
-        if (zoom <= 0) return Collections.emptyList();
-        Rectangle borderRect = null;
-        if (border != null) {
-            borderRect = tileToRect(border);
-        }
-        List<Tile> missedTiles = new LinkedList<>();
-        // The callers of this code *require* that we return any tiles
-        // that we do not draw in missedTiles.  ts.allExistingTiles() by
-        // default will only return already-existing tiles.  However, we
-        // need to return *all* tiles to the callers, so force creation
-        // here.
-        //boolean forceTileCreation = true;
-        for (Tile tile : ts.allTilesCreate()) {
-            Image img = getLoadedTileImage(tile);
-            if (img == null || tile.hasError()) {
-                if (Main.isDebugEnabled()) {
-                    Main.debug("missed tile: " + tile);
-                }
-                missedTiles.add(tile);
-                continue;
-            }
-            Rectangle sourceRect = tileToRect(tile);
-            if (borderRect != null && !sourceRect.intersects(borderRect)) {
-                continue;
-            }
-            drawImageInside(g, img, sourceRect, borderRect);
-        }
-        return missedTiles;
-    }
-
-    private void myDrawString(Graphics g, String text, int x, int y) {
-        Color oldColor = g.getColor();
-        g.setColor(Color.black);
-        g.drawString(text,x+1,y+1);
-        g.setColor(oldColor);
-        g.drawString(text,x,y);
-    }
-
-    private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
-        int fontHeight = g.getFontMetrics().getHeight();
-        if (tile == null)
-            return;
-        Point p = pixelPos(t);
-        int texty = p.y + 2 + fontHeight;
-
-        /*if (PROP_DRAW_DEBUG.get()) {
-            myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
-            texty += 1 + fontHeight;
-            if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
-                myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
-                texty += 1 + fontHeight;
-            }
-        }*/
-
-        if (tile == showMetadataTile) {
-            String md = tile.toString();
-            if (md != null) {
-                myDrawString(g, md, p.x + 2, texty);
-                texty += 1 + fontHeight;
-            }
-            Map<String, String> meta = tile.getMetadata();
-            if (meta != null) {
-                for (Map.Entry<String, String> entry : meta.entrySet()) {
-                    myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);
-                    texty += 1 + fontHeight;
-                }
-            }
-        }
-
-        /*String tileStatus = tile.getStatus();
-        if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
-            myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
-            texty += 1 + fontHeight;
-        }*/
-
-        if (tile.hasError() && showErrors) {
-            myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
-            texty += 1 + fontHeight;
-        }
-
-        /*int xCursor = -1;
-        int yCursor = -1;
-        if (PROP_DRAW_DEBUG.get()) {
-            if (yCursor < t.getYtile()) {
-                if (t.getYtile() % 32 == 31) {
-                    g.fillRect(0, p.y - 1, mv.getWidth(), 3);
-                } else {
-                    g.drawLine(0, p.y, mv.getWidth(), p.y);
-                }
-                yCursor = t.getYtile();
-            }
-            // This draws the vertical lines for the entire
-            // column. Only draw them for the top tile in
-            // the column.
-            if (xCursor < t.getXtile()) {
-                if (t.getXtile() % 32 == 0) {
-                    // level 7 tile boundary
-                    g.fillRect(p.x - 1, 0, 3, mv.getHeight());
-                } else {
-                    g.drawLine(p.x, 0, p.x, mv.getHeight());
-                }
-                xCursor = t.getXtile();
-            }
-        }*/
-    }
-
-    private Point pixelPos(LatLon ll) {
-        return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
-    }
-
-    private Point pixelPos(Tile t) {
-        double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());
-        LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);
-        return pixelPos(tmpLL);
-    }
-
-    private LatLon getShiftedLatLon(EastNorth en) {
-        return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
-    }
-
-    private Coordinate getShiftedCoord(EastNorth en) {
-        LatLon ll = getShiftedLatLon(en);
-        return new Coordinate(ll.lat(),ll.lon());
-    }
-
-    private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
-    private final class TileSet {
-        private int x0, x1, y0, y1;
-        private int zoom;
-        private int tileMax = -1;
-
-        /**
-         * Create a TileSet by EastNorth bbox taking a layer shift in account
-         */
-        private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
-            this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
-        }
-
-        /**
-         * Create a TileSet by known LatLon bbox without layer shift correction
-         */
-        private TileSet(LatLon topLeft, LatLon botRight, int zoom) {
-            this.zoom = zoom;
-            if (zoom == 0)
-                return;
-
-            x0 = (int)tileSource.lonToTileX(topLeft.lon(),  zoom);
-            y0 = (int)tileSource.latToTileY(topLeft.lat(),  zoom);
-            x1 = (int)tileSource.lonToTileX(botRight.lon(), zoom);
-            y1 = (int)tileSource.latToTileY(botRight.lat(), zoom);
-            if (x0 > x1) {
-                int tmp = x0;
-                x0 = x1;
-                x1 = tmp;
-            }
-            if (y0 > y1) {
-                int tmp = y0;
-                y0 = y1;
-                y1 = tmp;
-            }
-            tileMax = (int)Math.pow(2.0, zoom);
-            if (x0 < 0) {
-                x0 = 0;
-            }
-            if (y0 < 0) {
-                y0 = 0;
-            }
-            if (x1 > tileMax) {
-                x1 = tileMax;
-            }
-            if (y1 > tileMax) {
-                y1 = tileMax;
-            }
-        }
-
-        private boolean tooSmall() {
-            return this.tilesSpanned() < 2.1;
-        }
-
-        private boolean tooLarge() {
-            return this.tilesSpanned() > 10;
-        }
-
-        private boolean insane() {
-            return this.tilesSpanned() > 100;
-        }
-
-        private double tilesSpanned() {
-            return Math.sqrt(1.0 * this.size());
-        }
-
-        private int size() {
-            int x_span = x1 - x0 + 1;
-            int y_span = y1 - y0 + 1;
-            return x_span * y_span;
-        }
-
-        /*
-         * Get all tiles represented by this TileSet that are
-         * already in the tileCache.
-         */
-        private List<Tile> allExistingTiles() {
-            return this.__allTiles(false);
-        }
-
-        private List<Tile> allTilesCreate() {
-            return this.__allTiles(true);
-        }
-
-        private List<Tile> __allTiles(boolean create) {
-            // Tileset is either empty or too large
-            if (zoom == 0 || this.insane())
-                return Collections.emptyList();
-            List<Tile> ret = new ArrayList<>();
-            for (int x = x0; x <= x1; x++) {
-                for (int y = y0; y <= y1; y++) {
-                    Tile t;
-                    if (create) {
-                        t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
-                    } else {
-                        t = getTile(x % tileMax, y % tileMax, zoom);
-                    }
-                    if (t != null) {
-                        ret.add(t);
-                    }
-                }
-            }
-            return ret;
-        }
-
-        private List<Tile> allLoadedTiles() {
-            List<Tile> ret = new ArrayList<>();
-            for (Tile t : this.allExistingTiles()) {
-                if (t.isLoaded())
-                    ret.add(t);
-            }
-            return ret;
-        }
-
-        private Comparator<Tile> getTileDistanceComparator() {
-            final int centerX = (int) Math.ceil((x0 + x1) / 2);
-            final int centerY = (int) Math.ceil((y0 + y1) / 2);
-            return new Comparator<Tile>() {
-                private int getDistance(Tile t) {
-                    return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
-                }
-                @Override
-                public int compare(Tile o1, Tile o2) {
-                    int distance1 = getDistance(o1);
-                    int distance2 = getDistance(o2);
-                    return Integer.compare(distance1, distance2);
-                }
-            };
-        }
-
-        private void loadAllTiles(boolean force) {
-            if (!autoLoad && !force)
-                return;
-            List<Tile> allTiles = allTilesCreate();
-            Collections.sort(allTiles, getTileDistanceComparator());
-            for (Tile t : allTiles) { //, getTileDistanceComparator())) {
-                loadTile(t, false);
-            }
-        }
-
-        private void loadAllErrorTiles(boolean force) {
-            if (!autoLoad && !force)
-                return;
-            for (Tile t : this.allTilesCreate()) {
-                if (t.hasError()) {
-                    loadTile(t, true);
-                }
-            }
-        }
-    }
-
-
-    private static class TileSetInfo {
-        public boolean hasVisibleTiles = false;
-        public boolean hasOverzoomedTiles = false;
-        public boolean hasLoadingTiles = false;
-    }
-
-    private static TileSetInfo getTileSetInfo(TileSet ts) {
-        List<Tile> allTiles = ts.allExistingTiles();
-        TileSetInfo result = new TileSetInfo();
-        result.hasLoadingTiles = allTiles.size() < ts.size();
-        for (Tile t : allTiles) {
-            if (t.isLoaded()) {
-                if (!t.hasError()) {
-                    result.hasVisibleTiles = true;
-                }
-                if ("no-tile".equals(t.getValue("tile-info"))) {
-                    result.hasOverzoomedTiles = true;
-                }
-            } else {
-                result.hasLoadingTiles = true;
-            }
-        }
-        return result;
-    }
-
-    private class DeepTileSet {
-        private final EastNorth topLeft, botRight;
-        private final int minZoom, maxZoom;
-        private final TileSet[] tileSets;
-        private final TileSetInfo[] tileSetInfos;
-        public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
-            this.topLeft = topLeft;
-            this.botRight = botRight;
-            this.minZoom = minZoom;
-            this.maxZoom = maxZoom;
-            this.tileSets = new TileSet[maxZoom - minZoom + 1];
-            this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
-        }
-        public TileSet getTileSet(int zoom) {
-            if (zoom < minZoom)
-                return nullTileSet;
-            synchronized (tileSets) {
-                TileSet ts = tileSets[zoom-minZoom];
-                if (ts == null) {
-                    ts = new TileSet(topLeft, botRight, zoom);
-                    tileSets[zoom-minZoom] = ts;
-                }
-                return ts;
-            }
-        }
-
-        public TileSetInfo getTileSetInfo(int zoom) {
-            if (zoom < minZoom)
-                return new TileSetInfo();
-            synchronized (tileSetInfos) {
-                TileSetInfo tsi = tileSetInfos[zoom-minZoom];
-                if (tsi == null) {
-                    tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
-                    tileSetInfos[zoom-minZoom] = tsi;
-                }
-                return tsi;
-            }
-        }
-    }
-
-    @Override
-    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
-        EastNorth topLeft = mv.getEastNorth(0, 0);
-        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
-
-        if (botRight.east() == 0 || botRight.north() == 0) {
-            /*Main.debug("still initializing??");*/
-            // probably still initializing
-            return;
-        }
-
-        needRedraw = false;
-
-        int zoom = currentZoomLevel;
-        if (autoZoom) {
-            double pixelScaling = getScaleFactor(zoom);
-            if (pixelScaling > 3 || pixelScaling < 0.7) {
-                zoom = getBestZoom();
-            }
-        }
-
-        DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
-        TileSet ts = dts.getTileSet(zoom);
-
-        int displayZoomLevel = zoom;
-
-        boolean noTilesAtZoom = false;
-        if (autoZoom && autoLoad) {
-            // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
-            TileSetInfo tsi = dts.getTileSetInfo(zoom);
-            if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
-                noTilesAtZoom = true;
-            }
-            // Find highest zoom level with at least one visible tile
-            for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
-                if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
-                    displayZoomLevel = tmpZoom;
-                    break;
-                }
-            }
-            // Do binary search between currentZoomLevel and displayZoomLevel
-            while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
-                zoom = (zoom + displayZoomLevel)/2;
-                tsi = dts.getTileSetInfo(zoom);
-            }
-
-            setZoomLevel(zoom);
-
-            // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
-            // to make sure there're really no more zoom levels
-            if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
-                zoom++;
-                tsi = dts.getTileSetInfo(zoom);
-            }
-            // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
-            // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
-            while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
-                zoom--;
-                tsi = dts.getTileSetInfo(zoom);
-            }
-            ts = dts.getTileSet(zoom);
-        } else if (autoZoom) {
-            setZoomLevel(zoom);
-        }
-
-        // Too many tiles... refuse to download
-        if (!ts.tooLarge()) {
-            //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
-            ts.loadAllTiles(false);
-        }
-
-        if (displayZoomLevel != zoom) {
-            ts = dts.getTileSet(displayZoomLevel);
-        }
-
-        g.setColor(Color.DARK_GRAY);
-
-        List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
-        int[] otherZooms = { -1, 1, -2, 2, -3, -4, -5};
-        for (int zoomOffset : otherZooms) {
-            if (!autoZoom) {
-                break;
-            }
-            int newzoom = displayZoomLevel + zoomOffset;
-            if (newzoom < MIN_ZOOM) {
-                continue;
-            }
-            if (missedTiles.isEmpty()) {
-                break;
-            }
-            List<Tile> newlyMissedTiles = new LinkedList<>();
-            for (Tile missed : missedTiles) {
-                if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
-                    // Don't try to paint from higher zoom levels when tile is overzoomed
-                    newlyMissedTiles.add(missed);
-                    continue;
-                }
-                Tile t2 = tempCornerTile(missed);
-                LatLon topLeft2  = tileLatLon(missed);
-                LatLon botRight2 = tileLatLon(t2);
-                TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
-                // Instantiating large TileSets is expensive.  If there
-                // are no loaded tiles, don't bother even trying.
-                if (ts2.allLoadedTiles().isEmpty()) {
-                    newlyMissedTiles.add(missed);
-                    continue;
-                }
-                if (ts2.tooLarge()) {
-                    continue;
-                }
-                newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
-            }
-            missedTiles = newlyMissedTiles;
-        }
-        if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
-            Main.debug("still missed "+missedTiles.size()+" in the end");
-        }
-        g.setColor(Color.red);
-        g.setFont(InfoFont);
-
-        // The current zoom tileset should have all of its tiles
-        // due to the loadAllTiles(), unless it to tooLarge()
-        for (Tile t : ts.allExistingTiles()) {
-            this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
-        }
-
-        attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this);
-
-        //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
-        g.setColor(Color.lightGray);
-        if (!autoZoom) {
-            if (ts.insane()) {
-                myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
-            } else if (ts.tooLarge()) {
-                myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
-            } else if (ts.tooSmall()) {
-                myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
-            }
-        }
-        if (noTilesAtZoom) {
-            myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
-        }
-        if (Main.isDebugEnabled()) {
-            myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
-            myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
-            myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
-            myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
-            if(tileLoader instanceof TMSCachedTileLoader) {
-                TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;
-                int offset = 185;
-                for(String part: cachedTileLoader.getStats().split("\n")) {
-                    myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);
-                }
-
-            }
-        }
-    }
-
-    /**
-     * This isn't very efficient, but it is only used when the
-     * user right-clicks on the map.
-     */
-    private Tile getTileForPixelpos(int px, int py) {
-        if (Main.isDebugEnabled()) {
-            Main.debug("getTileForPixelpos("+px+", "+py+")");
-        }
-        MapView mv = Main.map.mapView;
-        Point clicked = new Point(px, py);
-        EastNorth topLeft = mv.getEastNorth(0, 0);
-        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
-        int z = currentZoomLevel;
-        TileSet ts = new TileSet(topLeft, botRight, z);
-
-        if (!ts.tooLarge()) {
-            ts.loadAllTiles(false); // make sure there are tile objects for all tiles
-        }
-        Tile clickedTile = null;
-        for (Tile t1 : ts.allExistingTiles()) {
-            Tile t2 = tempCornerTile(t1);
-            Rectangle r = new Rectangle(pixelPos(t1));
-            r.add(pixelPos(t2));
-            if (Main.isDebugEnabled()) {
-                Main.debug("r: " + r + " clicked: " + clicked);
-            }
-            if (!r.contains(clicked)) {
-                continue;
-            }
-            clickedTile  = t1;
-            break;
-        }
-        if (clickedTile == null)
-            return null;
-        /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
-                " currentZoomLevel: " + currentZoomLevel);*/
-        return clickedTile;
-    }
-
-    @Override
-    public Action[] getMenuEntries() {
-        return new Action[] {
-                LayerListDialog.getInstance().createShowHideLayerAction(),
-                LayerListDialog.getInstance().createDeleteLayerAction(),
-                SeparatorLayerAction.INSTANCE,
-                // color,
-                new OffsetAction(),
-                new RenameLayerAction(this.getAssociatedFile(), this),
-                SeparatorLayerAction.INSTANCE,
-                new LayerListPopup.InfoAction(this) };
-    }
-
-    @Override
-    public String getToolTipText() {
-        return tr("TMS layer ({0}), downloading in zoom {1}", getName(), currentZoomLevel);
-    }
-
-    @Override
-    public void visitBoundingBox(BoundingXYVisitor v) {
-    }
-
-    @Override
-    public boolean isChanged() {
-        return needRedraw;
-    }
 
     @Override
     public final boolean isProjectionSupported(Projection proj) {
diff --git a/src/org/openstreetmap/josm/gui/layer/WMSLayer.java b/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
index 3bc2a12..443a07b 100644
--- a/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
@@ -3,891 +3,72 @@ package org.openstreetmap.josm.gui.layer;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
-import java.awt.Component;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.Image;
-import java.awt.Point;
 import java.awt.event.ActionEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.image.BufferedImage;
-import java.awt.image.ImageObserver;
-import java.io.Externalizable;
-import java.io.File;
 import java.io.IOException;
-import java.io.InvalidClassException;
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
+import java.util.Arrays;
 import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.Map;
 
 import javax.swing.AbstractAction;
 import javax.swing.Action;
-import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
 
-import org.openstreetmap.gui.jmapviewer.AttributionSupport;
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.SaveActionBase;
-import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
-import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
-import org.openstreetmap.josm.data.ProjectionBounds;
-import org.openstreetmap.josm.data.coor.EastNorth;
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.imagery.GeorefImage;
-import org.openstreetmap.josm.data.imagery.GeorefImage.State;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
 import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
-import org.openstreetmap.josm.data.imagery.WmsCache;
-import org.openstreetmap.josm.data.imagery.types.ObjectFactory;
-import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
+import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
+import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
-import org.openstreetmap.josm.data.projection.Projection;
-import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
-import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
-import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
-import org.openstreetmap.josm.gui.progress.ProgressMonitor;
-import org.openstreetmap.josm.io.WMSLayerImporter;
-import org.openstreetmap.josm.io.imagery.HTMLGrabber;
-import org.openstreetmap.josm.io.imagery.WMSException;
-import org.openstreetmap.josm.io.imagery.WMSGrabber;
-import org.openstreetmap.josm.io.imagery.WMSRequest;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Utils;
 
 /**
  * This is a layer that grabs the current screen from an WMS server. The data
  * fetched this way is tiled and managed to the disc to reduce server load.
+ *
  */
-public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceChangedListener, Externalizable {
-
-    public static class PrecacheTask {
-        private final ProgressMonitor progressMonitor;
-        private volatile int totalCount;
-        private volatile int processedCount;
-        private volatile boolean isCancelled;
-
-        public PrecacheTask(ProgressMonitor progressMonitor) {
-            this.progressMonitor = progressMonitor;
-        }
-
-        public boolean isFinished() {
-            return totalCount == processedCount;
-        }
-
-        public int getTotalCount() {
-            return totalCount;
-        }
-
-        public void cancel() {
-            isCancelled = true;
-        }
-    }
-
-    // Fake reference to keep build scripts from removing ObjectFactory class. This class is not used directly but it's necessary for jaxb to work
-    @SuppressWarnings("unused")
-    private static final ObjectFactory OBJECT_FACTORY = null;
-
-    // these values correspond to the zoom levels used throughout OSM and are in meters/pixel from zoom level 0 to 18.
-    // taken from http://wiki.openstreetmap.org/wiki/Zoom_levels
-    private static final Double[] snapLevels = { 156412.0, 78206.0, 39103.0, 19551.0, 9776.0, 4888.0,
-        2444.0, 1222.0, 610.984, 305.492, 152.746, 76.373, 38.187, 19.093, 9.547, 4.773, 2.387, 1.193, 0.596 };
-
-    public static final BooleanProperty PROP_ALPHA_CHANNEL = new BooleanProperty("imagery.wms.alpha_channel", true);
-    public static final IntegerProperty PROP_SIMULTANEOUS_CONNECTIONS = new IntegerProperty("imagery.wms.simultaneousConnections", 3);
-    public static final BooleanProperty PROP_OVERLAP = new BooleanProperty("imagery.wms.overlap", false);
-    public static final IntegerProperty PROP_OVERLAP_EAST = new IntegerProperty("imagery.wms.overlapEast", 14);
-    public static final IntegerProperty PROP_OVERLAP_NORTH = new IntegerProperty("imagery.wms.overlapNorth", 4);
-    public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 500);
+public class WMSLayer extends AbstractTileSourceLayer {
+    /** default tile size for WMS Layer */
+    public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 512);
+    /** should WMS layer autozoom in default mode */
     public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty("imagery.wms.default_autozoom", true);
 
-    public int messageNum = 5; //limit for messages per layer
-    protected double resolution;
-    protected String resolutionText;
-    protected int imageSize;
-    protected int dax = 10;
-    protected int day = 10;
-    protected int daStep = 5;
-    protected int minZoom = 3;
-
-    protected GeorefImage[][] images;
-    protected static final int serializeFormatVersion = 5;
-    protected boolean autoDownloadEnabled = true;
-    protected boolean autoResolutionEnabled = PROP_DEFAULT_AUTOZOOM.get();
-    protected boolean settingsChanged;
-    public transient WmsCache cache;
-    private transient AttributionSupport attribution = new AttributionSupport();
-
-    // Image index boundary for current view
-    private volatile int bminx;
-    private volatile int bminy;
-    private volatile int bmaxx;
-    private volatile int bmaxy;
-    private volatile int leftEdge;
-    private volatile int bottomEdge;
-
-    // Request queue
-    private final transient List<WMSRequest> requestQueue = new ArrayList<>();
-    private final transient List<WMSRequest> finishedRequests = new ArrayList<>();
-    /**
-     * List of request currently being processed by download threads
-     */
-    private final transient List<WMSRequest> processingRequests = new ArrayList<>();
-    private final transient Lock requestQueueLock = new ReentrantLock();
-    private final transient Condition queueEmpty = requestQueueLock.newCondition();
-    private final transient List<WMSGrabber> grabbers = new ArrayList<>();
-    private final transient List<Thread> grabberThreads = new ArrayList<>();
-    private boolean canceled;
-
-    /** set to true if this layer uses an invalid base url */
-    private boolean usesInvalidUrl = false;
-    /** set to true if the user confirmed to use an potentially invalid WMS base url */
-    private boolean isInvalidUrlConfirmed = false;
-
-    /**
-     * Constructs a new {@code WMSLayer}.
-     */
-    public WMSLayer() {
-        this(new ImageryInfo(tr("Blank Layer")));
-    }
-
     /**
      * Constructs a new {@code WMSLayer}.
+     * @param info ImageryInfo description of the layer
      */
     public WMSLayer(ImageryInfo info) {
         super(info);
-        imageSize = PROP_IMAGE_SIZE.get();
-        setBackgroundLayer(true); /* set global background variable */
-        initializeImages();
-
-        attribution.initialize(this.info);
-
-        Main.pref.addPreferenceChangeListener(this);
-    }
-
-    @Override
-    public void hookUpMapView() {
-        if (info.getUrl() != null) {
-            startGrabberThreads();
-
-            for (WMSLayer layer: Main.map.mapView.getLayersOfType(WMSLayer.class)) {
-                if (layer.getInfo().getUrl().equals(info.getUrl())) {
-                    cache = layer.cache;
-                    break;
-                }
-            }
-            if (cache == null) {
-                cache = new WmsCache(info.getUrl(), imageSize);
-                cache.loadIndex();
-            }
-        }
-
-        // if automatic resolution is enabled, ensure that the first zoom level
-        // is already snapped. Otherwise it may load tiles that will never get
-        // used again when zooming.
-        updateResolutionSetting(this, autoResolutionEnabled);
-
-        final MouseAdapter adapter = new MouseAdapter() {
-            @Override
-            public void mouseClicked(MouseEvent e) {
-                if (!isVisible()) return;
-                if (e.getButton() == MouseEvent.BUTTON1) {
-                    attribution.handleAttribution(e.getPoint(), true);
-                }
-            }
-        };
-        Main.map.mapView.addMouseListener(adapter);
-
-        MapView.addLayerChangeListener(new LayerChangeListener() {
-            @Override
-            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
-                //
-            }
-
-            @Override
-            public void layerAdded(Layer newLayer) {
-                //
-            }
-
-            @Override
-            public void layerRemoved(Layer oldLayer) {
-                if (oldLayer == WMSLayer.this) {
-                    Main.map.mapView.removeMouseListener(adapter);
-                    MapView.removeLayerChangeListener(this);
-                }
-            }
-        });
-    }
-
-    public void doSetName(String name) {
-        setName(name);
-        info.setName(name);
-    }
-
-    public boolean hasAutoDownload(){
-        return autoDownloadEnabled;
-    }
-
-    public void setAutoDownload(boolean val) {
-        autoDownloadEnabled = val;
-    }
-
-    public boolean isAutoResolution() {
-        return autoResolutionEnabled;
-    }
-
-    public void setAutoResolution(boolean val) {
-        autoResolutionEnabled = val;
-    }
-
-    public void downloadAreaToCache(PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
-        Set<Point> requestedTiles = new HashSet<>();
-        for (LatLon point: points) {
-            EastNorth minEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() - bufferY, point.lon() - bufferX));
-            EastNorth maxEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() + bufferY, point.lon() + bufferX));
-            int minX = getImageXIndex(minEn.east());
-            int maxX = getImageXIndex(maxEn.east());
-            int minY = getImageYIndex(minEn.north());
-            int maxY = getImageYIndex(maxEn.north());
-
-            for (int x=minX; x<=maxX; x++) {
-                for (int y=minY; y<=maxY; y++) {
-                    requestedTiles.add(new Point(x, y));
-                }
-            }
-        }
-
-        for (Point p: requestedTiles) {
-            addRequest(new WMSRequest(p.x, p.y, info.getPixelPerDegree(), true, false, precacheTask));
-        }
-
-        precacheTask.progressMonitor.setTicksCount(precacheTask.getTotalCount());
-        precacheTask.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", 0, precacheTask.totalCount));
-    }
-
-    @Override
-    public void destroy() {
-        super.destroy();
-        cancelGrabberThreads(false);
-        Main.pref.removePreferenceChangeListener(this);
-        if (cache != null) {
-            cache.saveIndex();
-        }
-    }
-
-    public final void initializeImages() {
-        GeorefImage[][] old = images;
-        images = new GeorefImage[dax][day];
-        if (old != null) {
-            for (GeorefImage[] row : old) {
-                for (GeorefImage image : row) {
-                    images[modulo(image.getXIndex(), dax)][modulo(image.getYIndex(), day)] = image;
-                }
-            }
-        }
-        for(int x = 0; x<dax; ++x) {
-            for(int y = 0; y<day; ++y) {
-                if (images[x][y] == null) {
-                    images[x][y]= new GeorefImage(this);
-                }
-            }
-        }
-    }
-
-    @Override public ImageryInfo getInfo() {
-        return info;
-    }
-
-    @Override public String getToolTipText() {
-        if(autoDownloadEnabled)
-            return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolutionText);
-        else
-            return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolutionText);
-    }
-
-    private int modulo(int a, int b) {
-        return a % b >= 0 ? a%b : a%b+b;
-    }
-
-    private boolean zoomIsTooBig() {
-        //don't download when it's too outzoomed
-        return info.getPixelPerDegree() / getPPD() > minZoom;
     }
 
     @Override
-    public void paint(Graphics2D g, final MapView mv, Bounds b) {
-        if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
-
-        if (autoResolutionEnabled && !Utils.equalsEpsilon(getBestZoom(), mv.getDist100Pixel())) {
-            changeResolution(this, true);
-        }
-
-        settingsChanged = false;
-
-        ProjectionBounds bounds = mv.getProjectionBounds();
-        bminx= getImageXIndex(bounds.minEast);
-        bminy= getImageYIndex(bounds.minNorth);
-        bmaxx= getImageXIndex(bounds.maxEast);
-        bmaxy= getImageYIndex(bounds.maxNorth);
-
-        leftEdge = (int)(bounds.minEast * getPPD());
-        bottomEdge = (int)(bounds.minNorth * getPPD());
-
-        if (zoomIsTooBig()) {
-            for(int x = 0; x<images.length; ++x) {
-                for(int y = 0; y<images[0].length; ++y) {
-                    GeorefImage image = images[x][y];
-                    image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge);
-                }
-            }
-        } else {
-            downloadAndPaintVisible(g, mv, false);
+    public Action[] getMenuEntries() {
+        List<Action> ret = new ArrayList<>();
+        ret.addAll(Arrays.asList(super.getMenuEntries()));
+        ret.add(SeparatorLayerAction.INSTANCE);
+        ret.add(new LayerSaveAction(this));
+        ret.add(new LayerSaveAsAction(this));
+        ret.add(new BookmarkWmsAction());
+        return ret.toArray(new Action[]{});
     }
 
-        attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this);
-    }
 
     @Override
-    public void setOffset(double dx, double dy) {
-        super.setOffset(dx, dy);
-        settingsChanged = true;
-    }
-
-    public int getImageXIndex(double coord) {
-        return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize);
-    }
-
-    public int getImageYIndex(double coord) {
-        return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize);
-    }
-
-    public int getImageX(int imageIndex) {
-        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD());
-    }
-
-    public int getImageY(int imageIndex) {
-        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD());
-    }
-
-    public int getImageWidth(int xIndex) {
-        return getImageX(xIndex + 1) - getImageX(xIndex);
-    }
-
-    public int getImageHeight(int yIndex) {
-        return getImageY(yIndex + 1) - getImageY(yIndex);
-    }
-
-    /**
-     *
-     * @return Size of image in original zoom
-     */
-    public int getBaseImageWidth() {
-        int overlap = PROP_OVERLAP.get() ? PROP_OVERLAP_EAST.get() * imageSize / 100 : 0;
-        return imageSize + overlap;
-    }
-
-    /**
-     *
-     * @return Size of image in original zoom
-     */
-    public int getBaseImageHeight() {
-        int overlap = PROP_OVERLAP.get() ? PROP_OVERLAP_NORTH.get() * imageSize / 100 : 0;
-        return imageSize + overlap;
-    }
-
-    public int getImageSize() {
-        return imageSize;
-    }
-
-    public boolean isOverlapEnabled() {
-        return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0);
-    }
-
-    /**
-     *
-     * @return When overlapping is enabled, return visible part of tile. Otherwise return original image
-     */
-    public BufferedImage normalizeImage(BufferedImage img) {
-        if (isOverlapEnabled()) {
-            BufferedImage copy = img;
-            img = new BufferedImage(imageSize, imageSize, copy.getType());
-            img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize,
-                    0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null);
-        }
-        return img;
-    }
-
-    /**
-     *
-     * @param xIndex
-     * @param yIndex
-     * @return Real EastNorth of given tile. dx/dy is not counted in
-     */
-    public EastNorth getEastNorth(int xIndex, int yIndex) {
-        return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree());
-    }
-
-    protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){
-
-        int newDax = dax;
-        int newDay = day;
-
-        if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) {
-            newDax = ((bmaxx - bminx) / daStep + 1) * daStep;
-        }
-
-        if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) {
-            newDay = ((bmaxy - bminy) / daStep + 1) * daStep;
-        }
-
-        if (newDax != dax || newDay != day) {
-            dax = newDax;
-            day = newDay;
-            initializeImages();
-        }
-
-        for(int x = bminx; x<=bmaxx; ++x) {
-            for(int y = bminy; y<=bmaxy; ++y){
-                images[modulo(x,dax)][modulo(y,day)].changePosition(x, y);
-            }
-        }
-
-        gatherFinishedRequests();
-        Set<ProjectionBounds> areaToCache = new HashSet<>();
-
-        for(int x = bminx; x<=bmaxx; ++x) {
-            for(int y = bminy; y<=bmaxy; ++y){
-                GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
-                if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) {
-                    addRequest(new WMSRequest(x, y, info.getPixelPerDegree(), real, true));
-                    areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
-                } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) {
-                    addRequest(new WMSRequest(x, y, info.getPixelPerDegree(), real, false));
-                    areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
-                }
-            }
-        }
-        if (cache != null) {
-            cache.setAreaToCache(areaToCache);
-        }
-    }
-
-    @Override public void visitBoundingBox(BoundingXYVisitor v) {
-        for(int x = 0; x<dax; ++x) {
-            for(int y = 0; y<day; ++y)
-                if(images[x][y].getImage() != null){
-                    v.visit(images[x][y].getMin());
-                    v.visit(images[x][y].getMax());
-                }
-        }
-    }
-
-    @Override public Action[] getMenuEntries() {
-        return new Action[]{
-                LayerListDialog.getInstance().createActivateLayerAction(this),
-                LayerListDialog.getInstance().createShowHideLayerAction(),
-                LayerListDialog.getInstance().createDeleteLayerAction(),
-                SeparatorLayerAction.INSTANCE,
-                new OffsetAction(),
-                new LayerSaveAction(this),
-                new LayerSaveAsAction(this),
-                new BookmarkWmsAction(),
-                SeparatorLayerAction.INSTANCE,
-                new StartStopAction(),
-                new ToggleAlphaAction(),
-                new ToggleAutoResolutionAction(),
-                new ChangeResolutionAction(),
-                new ZoomToNativeResolution(),
-                new ReloadErrorTilesAction(),
-                new DownloadAction(),
-                SeparatorLayerAction.INSTANCE,
-                new LayerListPopup.InfoAction(this)
-        };
+    public TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
+        if (info.getImageryType() == ImageryType.WMS && info.getUrl() != null) {
+            TemplatedWMSTileSource.checkUrl(info.getUrl());
+            TemplatedWMSTileSource tileSource = new TemplatedWMSTileSource(info);
+            info.setAttribution(tileSource);
+            return tileSource;
         }
-
-    public GeorefImage findImage(EastNorth eastNorth) {
-        int xIndex = getImageXIndex(eastNorth.east());
-        int yIndex = getImageYIndex(eastNorth.north());
-        GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];
-        if (result.getXIndex() == xIndex && result.getYIndex() == yIndex)
-            return result;
-        else
         return null;
     }
 
     /**
-     *
-     * @param request
-     * @return -1 if request is no longer needed, otherwise priority of request (lower number &lt;=&gt; more important request)
-     */
-    private int getRequestPriority(WMSRequest request) {
-        if (!Utils.equalsEpsilon(request.getPixelPerDegree(), info.getPixelPerDegree()))
-            return -1;
-        if (bminx > request.getXIndex()
-                || bmaxx < request.getXIndex()
-                || bminy > request.getYIndex()
-                || bmaxy < request.getYIndex())
-            return -1;
-
-        MouseEvent lastMEvent = Main.map.mapView.lastMEvent;
-        EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY());
-        int mouseX = getImageXIndex(cursorEastNorth.east());
-        int mouseY = getImageYIndex(cursorEastNorth.north());
-        int dx = request.getXIndex() - mouseX;
-        int dy = request.getYIndex() - mouseY;
-
-        return 1 + dx * dx + dy * dy;
-    }
-
-    private void sortRequests(boolean localOnly) {
-        Iterator<WMSRequest> it = requestQueue.iterator();
-        while (it.hasNext()) {
-            WMSRequest item = it.next();
-
-            if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) {
-                it.remove();
-                continue;
-            }
-
-            int priority = getRequestPriority(item);
-            if (priority == -1 && item.isPrecacheOnly()) {
-                priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view
-            }
-
-            if (localOnly && !item.hasExactMatch()) {
-                priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately
-            }
-
-            if (       priority == -1
-                    || finishedRequests.contains(item)
-                    || processingRequests.contains(item)) {
-                it.remove();
-            } else {
-                item.setPriority(priority);
-            }
-        }
-        Collections.sort(requestQueue);
-    }
-
-    public WMSRequest getRequest(boolean localOnly) {
-        requestQueueLock.lock();
-        try {
-            sortRequests(localOnly);
-            while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) {
-                try {
-                    queueEmpty.await();
-                    sortRequests(localOnly);
-                } catch (InterruptedException e) {
-                    Main.warn("InterruptedException in "+getClass().getSimpleName()+" during WMS request");
-                }
-            }
-
-            if (canceled)
-                return null;
-            else {
-                WMSRequest request = requestQueue.remove(0);
-                processingRequests.add(request);
-                return request;
-            }
-
-        } finally {
-            requestQueueLock.unlock();
-        }
-    }
-
-    public void finishRequest(WMSRequest request) {
-        requestQueueLock.lock();
-        try {
-            PrecacheTask task = request.getPrecacheTask();
-            if (task != null) {
-                task.processedCount++;
-                if (!task.progressMonitor.isCanceled()) {
-                    task.progressMonitor.worked(1);
-                    task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount));
-                }
-            }
-            processingRequests.remove(request);
-            if (request.getState() != null && !request.isPrecacheOnly()) {
-                finishedRequests.add(request);
-                if (Main.isDisplayingMapView()) {
-                    Main.map.mapView.repaint();
-                }
-            }
-        } finally {
-            requestQueueLock.unlock();
-        }
-    }
-
-    public void addRequest(WMSRequest request) {
-        requestQueueLock.lock();
-        try {
-
-            if (cache != null) {
-                ProjectionBounds b = getBounds(request);
-                // Checking for exact match is fast enough, no need to do it in separated thread
-                request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));
-                if (request.isPrecacheOnly() && request.hasExactMatch())
-                    return; // We already have this tile cached
-            }
-
-            if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {
-                requestQueue.add(request);
-                if (request.getPrecacheTask() != null) {
-                    request.getPrecacheTask().totalCount++;
-                }
-                queueEmpty.signalAll();
-            }
-        } finally {
-            requestQueueLock.unlock();
-        }
-    }
-
-    public boolean requestIsVisible(WMSRequest request) {
-        return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
-    }
-
-    private void gatherFinishedRequests() {
-        requestQueueLock.lock();
-        try {
-            for (WMSRequest request: finishedRequests) {
-                GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
-                if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
-                    WMSException we = request.getException();
-                    img.changeImage(request.getState(), request.getImage(), we != null ? we.getMessage() : null);
-                }
-            }
-        } finally {
-            requestQueueLock.unlock();
-            finishedRequests.clear();
-        }
-    }
-
-    public class DownloadAction extends AbstractAction {
-        /**
-         * Constructs a new {@code DownloadAction}.
-         */
-        public DownloadAction() {
-            super(tr("Download visible tiles"));
-        }
-        @Override
-        public void actionPerformed(ActionEvent ev) {
-            if (zoomIsTooBig()) {
-                JOptionPane.showMessageDialog(
-                        Main.parent,
-                        tr("The requested area is too big. Please zoom in a little, or change resolution"),
-                        tr("Error"),
-                        JOptionPane.ERROR_MESSAGE
-                        );
-            } else {
-                downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);
-            }
-        }
-    }
-
-    /**
-     * Finds the most suitable resolution for the current zoom level, but prefers
-     * higher resolutions. Snaps to values defined in snapLevels.
-     * @return best zoom level
-     */
-    private static double getBestZoom() {
-        // not sure why getDist100Pixel returns values corresponding to
-        // the snapLevels, which are in meters per pixel. It works, though.
-        double dist = Main.map.mapView.getDist100Pixel();
-        for(int i = snapLevels.length-2; i >= 0; i--) {
-            if(snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist)
-                return snapLevels[i+1];
-        }
-        return snapLevels[0];
-    }
-
-    /**
-     * Updates the given layer’s resolution settings to the current zoom level. Does
-     * not update existing tiles, only new ones will be subject to the new settings.
-     *
-     * @param layer
-     * @param snap  Set to true if the resolution should snap to certain values instead of
-     *              matching the current zoom level perfectly
-     */
-    private static void updateResolutionSetting(WMSLayer layer, boolean snap) {
-        if(snap) {
-            layer.resolution = getBestZoom();
-            layer.resolutionText = MapView.getDistText(layer.resolution);
-        } else {
-            layer.resolution = Main.map.mapView.getDist100Pixel();
-            layer.resolutionText = Main.map.mapView.getDist100PixelText();
-        }
-        layer.info.setPixelPerDegree(layer.getPPD());
-    }
-
-    /**
-     * Updates the given layer’s resolution settings to the current zoom level and
-     * updates existing tiles. If round is true, tiles will be updated gradually, if
-     * false they will be removed instantly (and redrawn only after the new resolution
-     * image has been loaded).
-     * @param layer
-     * @param snap  Set to true if the resolution should snap to certain values instead of
-     *              matching the current zoom level perfectly
-     */
-    private static void changeResolution(WMSLayer layer, boolean snap) {
-        updateResolutionSetting(layer, snap);
-
-        layer.settingsChanged = true;
-
-        // Don’t move tiles off screen when the resolution is rounded. This
-        // prevents some flickering when zooming with auto-resolution enabled
-        // and instead gradually updates each tile.
-        if(!snap) {
-            for(int x = 0; x<layer.dax; ++x) {
-                for(int y = 0; y<layer.day; ++y) {
-                    layer.images[x][y].changePosition(-1, -1);
-                }
-            }
-        }
-    }
-
-    public static class ChangeResolutionAction extends AbstractAction implements LayerAction {
-
-        /**
-         * Constructs a new {@code ChangeResolutionAction}
-         */
-        public ChangeResolutionAction() {
-            super(tr("Change resolution"));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent ev) {
-            List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
-            for (Layer l: layers) {
-                changeResolution((WMSLayer) l, false);
-            }
-            Main.map.mapView.repaint();
-        }
-
-        @Override
-        public boolean supportLayers(List<Layer> layers) {
-            for (Layer l: layers) {
-                if (!(l instanceof WMSLayer))
-                    return false;
-            }
-            return true;
-        }
-
-        @Override
-        public Component createMenuComponent() {
-            return new JMenuItem(this);
-        }
-    }
-
-    public class ReloadErrorTilesAction extends AbstractAction {
-        /**
-         * Constructs a new {@code ReloadErrorTilesAction}.
-         */
-        public ReloadErrorTilesAction() {
-            super(tr("Reload erroneous tiles"));
-        }
-        @Override
-        public void actionPerformed(ActionEvent ev) {
-            // Delete small files, because they're probably blank tiles.
-            // See #2307
-            cache.cleanSmallFiles(4096);
-
-            for (int x = 0; x < dax; ++x) {
-                for (int y = 0; y < day; ++y) {
-                    GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
-                    if(img.getState() == State.FAILED){
-                        addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false));
-                    }
-                }
-            }
-        }
-    }
-
-    public class ToggleAlphaAction extends AbstractAction implements LayerAction {
-        /**
-         * Constructs a new {@code ToggleAlphaAction}.
-         */
-        public ToggleAlphaAction() {
-            super(tr("Alpha channel"));
-        }
-        @Override
-        public void actionPerformed(ActionEvent ev) {
-            JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
-            boolean alphaChannel = checkbox.isSelected();
-            PROP_ALPHA_CHANNEL.put(alphaChannel);
-            Main.info("WMS Alpha channel changed to "+alphaChannel);
-
-            // clear all resized cached instances and repaint the layer
-            for (int x = 0; x < dax; ++x) {
-                for (int y = 0; y < day; ++y) {
-                    GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
-                    img.flushResizedCachedInstance();
-                    BufferedImage bi = img.getImage();
-                    // Completely erases images for which transparency has been forced,
-                    // or images that should be forced now, as they need to be recreated
-                    if (ImageProvider.isTransparencyForced(bi) || ImageProvider.hasTransparentColor(bi)) {
-                        img.resetImage();
-                    }
-                }
-            }
-            Main.map.mapView.repaint();
-        }
-
-        @Override
-        public Component createMenuComponent() {
-            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
-            item.setSelected(PROP_ALPHA_CHANNEL.get());
-            return item;
-        }
-
-        @Override
-        public boolean supportLayers(List<Layer> layers) {
-            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
-        }
-    }
-
-    public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction {
-
-        /**
-         * Constructs a new {@code ToggleAutoResolutionAction}.
-         */
-        public ToggleAutoResolutionAction() {
-            super(tr("Automatically change resolution"));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent ev) {
-            JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
-            autoResolutionEnabled = checkbox.isSelected();
-        }
-
-        @Override
-        public Component createMenuComponent() {
-            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
-            item.setSelected(autoResolutionEnabled);
-            return item;
-        }
-
-        @Override
-        public boolean supportLayers(List<Layer> layers) {
-            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
-        }
-    }
-
-    /**
      * This action will add a WMS layer menu entry with the current WMS layer
      * URL and name extended by the current resolution.
      * When using the menu entry again, the WMS cache will be used properly.
@@ -905,123 +86,6 @@ public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceC
         }
     }
 
-    private class StartStopAction extends AbstractAction implements LayerAction {
-
-        public StartStopAction() {
-            super(tr("Automatic downloading"));
-        }
-
-        @Override
-        public Component createMenuComponent() {
-            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
-            item.setSelected(autoDownloadEnabled);
-            return item;
-        }
-
-        @Override
-        public boolean supportLayers(List<Layer> layers) {
-            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            autoDownloadEnabled = !autoDownloadEnabled;
-            if (autoDownloadEnabled) {
-                for (int x = 0; x < dax; ++x) {
-                    for (int y = 0; y < day; ++y) {
-                        GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
-                        if(img.getState() == State.NOT_IN_CACHE){
-                            addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));
-                        }
-                    }
-                }
-                Main.map.mapView.repaint();
-            }
-        }
-    }
-
-    private class ZoomToNativeResolution extends AbstractAction {
-
-        public ZoomToNativeResolution() {
-            super(tr("Zoom to native resolution"));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
-        }
-    }
-
-    private void cancelGrabberThreads(boolean wait) {
-        requestQueueLock.lock();
-        try {
-            canceled = true;
-            for (WMSGrabber grabber: grabbers) {
-                grabber.cancel();
-            }
-            queueEmpty.signalAll();
-        } finally {
-            requestQueueLock.unlock();
-        }
-        if (wait) {
-            for (Thread t: grabberThreads) {
-                try {
-                    t.join();
-                } catch (InterruptedException e) {
-                    Main.warn("InterruptedException in "+getClass().getSimpleName()+" while cancelling grabber threads");
-                }
-            }
-        }
-    }
-
-    private void startGrabberThreads() {
-        int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
-        requestQueueLock.lock();
-        try {
-            canceled = false;
-            grabbers.clear();
-            grabberThreads.clear();
-            for (int i=0; i<threadCount; i++) {
-                WMSGrabber grabber = getGrabber(i == 0 && threadCount > 1);
-                grabbers.add(grabber);
-                Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
-                t.setDaemon(true);
-                t.start();
-                grabberThreads.add(t);
-            }
-        } finally {
-            requestQueueLock.unlock();
-        }
-    }
-
-    @Override
-    public boolean isChanged() {
-        requestQueueLock.lock();
-        try {
-            return !finishedRequests.isEmpty() || settingsChanged;
-        } finally {
-            requestQueueLock.unlock();
-        }
-    }
-
-    @Override
-    public void preferenceChanged(PreferenceChangeEvent event) {
-        if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey()) && info.getUrl() != null) {
-            cancelGrabberThreads(true);
-            startGrabberThreads();
-        } else if (
-                event.getKey().equals(PROP_OVERLAP.getKey())
-                || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
-                || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
-            for (int i=0; i<images.length; i++) {
-                for (int k=0; k<images[i].length; k++) {
-                    images[i][k] = new GeorefImage(this);
-                }
-            }
-
-            settingsChanged = true;
-        }
-    }
 
     /**
      * Checks that WMS layer is a grabber-compatible one (HTML or WMS).
@@ -1029,124 +93,27 @@ public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceC
      * @since 8068
      */
     public void checkGrabberType() {
-        ImageryType it = getInfo().getImageryType();
-        if (!ImageryType.HTML.equals(it) && !ImageryType.WMS.equals(it))
-            throw new IllegalStateException("getGrabber() called for non-WMS layer type");
-    }
-
-    protected WMSGrabber getGrabber(boolean localOnly) {
-        checkGrabberType();
-        if (getInfo().getImageryType() == ImageryType.HTML)
-            return new HTMLGrabber(Main.map.mapView, this, localOnly);
-        else
-            return new WMSGrabber(Main.map.mapView, this, localOnly);
-    }
-
-    public ProjectionBounds getBounds(WMSRequest request) {
-        ProjectionBounds result = new ProjectionBounds(
-                getEastNorth(request.getXIndex(), request.getYIndex()),
-                getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
-
-        if (WMSLayer.PROP_OVERLAP.get()) {
-            double eastSize =  result.maxEast - result.minEast;
-            double northSize =  result.maxNorth - result.minNorth;
-
-            double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
-            double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
-
-            result = new ProjectionBounds(result.getMin(),
-                    new EastNorth(result.maxEast + eastCoef * eastSize,
-                            result.maxNorth + northCoef * northSize));
-        }
-        return result;
     }
 
+    private static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("WMS") {
         @Override
-    public boolean isProjectionSupported(Projection proj) {
-        List<String> serverProjections = info.getServerProjections();
-        return serverProjections.contains(proj.toCode().toUpperCase(Locale.ENGLISH))
-                || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84")))
-                || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84"));
+        protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout,
+                int readTimeout, Map<String, String> headers, String cacheDir) throws IOException {
+            return new WMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir);
         }
 
-    @Override
-    public String nameSupportedProjections() {
-        StringBuilder res = new StringBuilder();
-        for (String p : info.getServerProjections()) {
-            if (res.length() > 0) {
-                res.append(", ");
-            }
-            res.append(p);
-        }
-        return tr("Supported projections are: {0}", res);
-    }
-
-    @Override
-    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
-        boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
-        Main.map.repaint(done ? 0 : 100);
-        return !done;
-    }
-
-    @Override
-    public void writeExternal(ObjectOutput out) throws IOException {
-        out.writeInt(serializeFormatVersion);
-        out.writeInt(dax);
-        out.writeInt(day);
-        out.writeInt(imageSize);
-        out.writeDouble(info.getPixelPerDegree());
-        out.writeObject(info.getName());
-        out.writeObject(info.getExtendedUrl());
-        out.writeObject(images);
-    }
-
-    @Override
-    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
-        int sfv = in.readInt();
-        if (sfv != serializeFormatVersion)
-            throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion));
-        autoDownloadEnabled = false;
-        dax = in.readInt();
-        day = in.readInt();
-        imageSize = in.readInt();
-        info.setPixelPerDegree(in.readDouble());
-        doSetName((String)in.readObject());
-        info.setExtendedUrl((String)in.readObject());
-        images = (GeorefImage[][])in.readObject();
-
-        for (GeorefImage[] imgs : images) {
-            for (GeorefImage img : imgs) {
-                if (img != null) {
-                    img.setLayer(WMSLayer.this);
-                }
-            }
-        }
-
-        settingsChanged = true;
-        if (Main.isDisplayingMapView()) {
-            Main.map.mapView.repaint();
-        }
-        if (cache != null) {
-            cache.saveIndex();
-            cache = null;
-        }
-    }
+    };
 
     @Override
-    public void onPostLoadFromFile() {
-        if (info.getUrl() != null) {
-            cache = new WmsCache(info.getUrl(), imageSize);
-            startGrabberThreads();
-        }
+    protected TileLoaderFactory getTileLoaderFactory() {
+        return loaderFactory;
     }
 
     @Override
-    public boolean isSavable() {
-        return true; // With WMSLayerExporter
+    protected Map<String, String> getHeaders(TileSource tileSource) {
+        if (tileSource instanceof TemplatedWMSTileSource) {
+            return ((TemplatedWMSTileSource)tileSource).getHeaders();
         }
-
-    @Override
-    public File createAndOpenSaveFileChooser() {
-        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
+        return null;
     }
 }
diff --git a/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java b/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java
index ad63bbb..995cd17 100644
--- a/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java
+++ b/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java
@@ -24,8 +24,8 @@ import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
 import org.openstreetmap.josm.data.gpx.WayPoint;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
-import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask;
+import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
+import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer.PrecacheTask;
 import org.openstreetmap.josm.gui.progress.ProgressTaskId;
 import org.openstreetmap.josm.gui.progress.ProgressTaskIds;
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
@@ -34,10 +34,17 @@ import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.xml.sax.SAXException;
 
+/**
+ * Class downloading WMS and TMS along the GPX track
+ *
+ */
 public class DownloadWmsAlongTrackAction extends AbstractAction {
 
     private final transient GpxData data;
 
+    /**
+     * @param data that represents GPX track, along which data should be downloaded
+     */
     public DownloadWmsAlongTrackAction(final GpxData data) {
         super(tr("Precache imagery tiles along this track"), ImageProvider.get("downloadalongtrack"));
         this.data = data;
@@ -56,14 +63,14 @@ public class DownloadWmsAlongTrackAction extends AbstractAction {
         for (WayPoint p : data.waypoints) {
             points.add(p.getCoor());
         }
-        final WMSLayer layer = askWMSLayer();
+        final AbstractTileSourceLayer layer = askedLayer();
         if (layer != null) {
             PleaseWaitRunnable task = new PleaseWaitRunnable(tr("Precaching WMS")) {
                 private PrecacheTask precacheTask;
 
                 @Override
                 protected void realRun() throws SAXException, IOException, OsmTransferException {
-                    precacheTask = new PrecacheTask(progressMonitor);
+                    precacheTask = layer.new PrecacheTask(progressMonitor);
                     layer.downloadAreaToCache(precacheTask, points, 0, 0);
                     while (!precacheTask.isFinished() && !progressMonitor.isCanceled()) {
                         synchronized (this) {
@@ -94,13 +101,13 @@ public class DownloadWmsAlongTrackAction extends AbstractAction {
         }
     }
 
-    protected WMSLayer askWMSLayer() {
-        Collection<WMSLayer> targetLayers = Main.map.mapView.getLayersOfType(WMSLayer.class);
+    protected AbstractTileSourceLayer askedLayer() {
+        Collection<AbstractTileSourceLayer> targetLayers = Main.map.mapView.getLayersOfType(AbstractTileSourceLayer.class);
         if (targetLayers.isEmpty()) {
             warnNoImageryLayers();
             return null;
         }
-        JosmComboBox<WMSLayer> layerList = new JosmComboBox<>(targetLayers.toArray(new WMSLayer[0]));
+        JosmComboBox<AbstractTileSourceLayer> layerList = new JosmComboBox<>(targetLayers.toArray(new AbstractTileSourceLayer[0]));
         layerList.setRenderer(new LayerListCellRenderer());
         layerList.setSelectedIndex(0);
         JPanel pnl = new JPanel(new GridBagLayout());
@@ -113,7 +120,7 @@ public class DownloadWmsAlongTrackAction extends AbstractAction {
         if (ed.getValue() != 1) {
             return null;
         }
-        return (WMSLayer) layerList.getSelectedItem();
+        return (AbstractTileSourceLayer) layerList.getSelectedItem();
     }
 
     protected void warnNoImageryLayers() {
diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java
index 96c2c59..ed5168b 100644
--- a/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java
+++ b/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java
@@ -11,6 +11,7 @@ import javax.swing.JPanel;
 import javax.swing.JSpinner;
 import javax.swing.SpinnerNumberModel;
 
+import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
 import org.openstreetmap.josm.gui.layer.TMSLayer;
@@ -40,8 +41,8 @@ public class TMSSettingsPanel extends JPanel {
      */
     public TMSSettingsPanel() {
         super(new GridBagLayout());
-        minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MIN_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
-        maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MAX_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
+        minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.PROP_MIN_ZOOM_LVL.get().intValue(), TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
+        maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.PROP_MAX_ZOOM_LVL.get().intValue(), TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
         maxElementsOnDisk = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get().intValue(), 0, Integer.MAX_VALUE, 1));
         maxConcurrentDownloads = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoaderJob.THREAD_LIMIT.get().intValue(), 0, Integer.MAX_VALUE, 1));
         maxDownloadsPerHost = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoader.HOST_LIMIT.get().intValue(), 0, Integer.MAX_VALUE, 1));
@@ -94,7 +95,7 @@ public class TMSSettingsPanel extends JPanel {
         this.addToSlippyMapChosser.setSelected(TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get());
         this.maxZoomLvl.setValue(TMSLayer.getMaxZoomLvl(null));
         this.minZoomLvl.setValue(TMSLayer.getMinZoomLvl(null));
-        this.tilecacheDir.setText(TMSLayer.PROP_TILECACHE_DIR.get());
+        this.tilecacheDir.setText(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get());
         this.maxElementsOnDisk.setValue(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get());
         this.maxConcurrentDownloads.setValue(TMSCachedTileLoaderJob.THREAD_LIMIT.get());
         this.maxDownloadsPerHost.setValue(TMSCachedTileLoader.HOST_LIMIT.get());
@@ -131,9 +132,9 @@ public class TMSSettingsPanel extends JPanel {
             restartRequired = true;
         }
 
-        if (!TMSLayer.PROP_TILECACHE_DIR.get().equals(this.tilecacheDir.getText())) {
+        if (!CachedTileLoaderFactory.PROP_TILECACHE_DIR.get().equals(this.tilecacheDir.getText())) {
             restartRequired = true;
-            TMSLayer.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText());
+            CachedTileLoaderFactory.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText());
         }
 
         return restartRequired;
diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java
index 044c91b..dfe1e44 100644
--- a/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java
+++ b/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java
@@ -3,7 +3,6 @@ package org.openstreetmap.josm.gui.preferences.imagery;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
-import java.awt.FlowLayout;
 import java.awt.GridBagLayout;
 
 import javax.swing.Box;
@@ -13,9 +12,8 @@ import javax.swing.JPanel;
 import javax.swing.JSpinner;
 import javax.swing.SpinnerNumberModel;
 
+import org.openstreetmap.josm.data.imagery.WMSCachedTileLoaderJob;
 import org.openstreetmap.josm.gui.layer.WMSLayer;
-import org.openstreetmap.josm.gui.widgets.JosmComboBox;
-import org.openstreetmap.josm.io.imagery.HTMLGrabber;
 import org.openstreetmap.josm.tools.GBC;
 
 /**
@@ -26,11 +24,8 @@ public class WMSSettingsPanel extends JPanel {
 
     // WMS Settings
     private final JCheckBox autozoomActive;
-    private final JosmComboBox<String> browser;
-    private final JCheckBox overlapCheckBox;
-    private final JSpinner spinEast;
-    private final JSpinner spinNorth;
     private final JSpinner spinSimConn;
+    private final JSpinner tileSize;
 
     /**
      * Constructs a new {@code WMSSettingsPanel}.
@@ -44,42 +39,22 @@ public class WMSSettingsPanel extends JPanel {
         add(GBC.glue(5, 0), GBC.std());
         add(autozoomActive, GBC.eol().fill(GBC.HORIZONTAL));
 
-        // Downloader
-        browser = new JosmComboBox<>(new String[] {
-                "webkit-image {0}",
-                "gnome-web-photo --mode=photo --format=png {0} /dev/stdout",
-                "gnome-web-photo-fixed {0}",
-        "webkit-image-gtk {0}"});
-        browser.setEditable(true);
-        add(new JLabel(tr("Downloader:")), GBC.std());
-        add(GBC.glue(5, 0), GBC.std());
-        add(browser, GBC.eol().fill(GBC.HORIZONTAL));
-
         // Simultaneous connections
         add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL));
         JLabel labelSimConn = new JLabel(tr("Simultaneous connections:"));
-        spinSimConn = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.get().intValue(), 1, 30, 1));
+        spinSimConn = new JSpinner(new SpinnerNumberModel(WMSCachedTileLoaderJob.THREAD_LIMIT.get().intValue(), 1, 30, 1));
+        labelSimConn.setLabelFor(spinSimConn);
         add(labelSimConn, GBC.std());
         add(GBC.glue(5, 0), GBC.std());
         add(spinSimConn, GBC.eol());
 
-        // Overlap
-        add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL));
-
-        overlapCheckBox = new JCheckBox(tr("Overlap tiles"));
-        JLabel labelEast = new JLabel(tr("% of east:"));
-        JLabel labelNorth = new JLabel(tr("% of north:"));
-        spinEast = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_EAST.get().intValue(), 1, 50, 1));
-        spinNorth = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_NORTH.get().intValue(), 1, 50, 1));
-
-        JPanel overlapPanel = new JPanel(new FlowLayout());
-        overlapPanel.add(overlapCheckBox);
-        overlapPanel.add(labelEast);
-        overlapPanel.add(spinEast);
-        overlapPanel.add(labelNorth);
-        overlapPanel.add(spinNorth);
-
-        add(overlapPanel, GBC.eop());
+        // Tile size
+        JLabel labelTileSize = new JLabel(tr("Tile size:"));
+        tileSize = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_IMAGE_SIZE.get().intValue(), 1, 4096, 128));
+        labelTileSize.setLabelFor(tileSize);
+        add(labelTileSize, GBC.std());
+        add(GBC.glue(5, 0), GBC.std());
+        add(tileSize, GBC.eol());
     }
 
     /**
@@ -87,11 +62,8 @@ public class WMSSettingsPanel extends JPanel {
      */
     public void loadSettings() {
         this.autozoomActive.setSelected(WMSLayer.PROP_DEFAULT_AUTOZOOM.get());
-        this.browser.setSelectedItem(HTMLGrabber.PROP_BROWSER.get());
-        this.overlapCheckBox.setSelected(WMSLayer.PROP_OVERLAP.get());
-        this.spinEast.setValue(WMSLayer.PROP_OVERLAP_EAST.get());
-        this.spinNorth.setValue(WMSLayer.PROP_OVERLAP_NORTH.get());
-        this.spinSimConn.setValue(WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.get());
+        this.spinSimConn.setValue(WMSCachedTileLoaderJob.THREAD_LIMIT.get());
+        this.tileSize.setValue(WMSLayer.PROP_IMAGE_SIZE.get());
     }
 
     /**
@@ -100,12 +72,8 @@ public class WMSSettingsPanel extends JPanel {
      */
     public boolean saveSettings() {
         WMSLayer.PROP_DEFAULT_AUTOZOOM.put(this.autozoomActive.isSelected());
-        WMSLayer.PROP_OVERLAP.put(overlapCheckBox.getModel().isSelected());
-        WMSLayer.PROP_OVERLAP_EAST.put((Integer) spinEast.getModel().getValue());
-        WMSLayer.PROP_OVERLAP_NORTH.put((Integer) spinNorth.getModel().getValue());
-        WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.put((Integer) spinSimConn.getModel().getValue());
-
-        HTMLGrabber.PROP_BROWSER.put(browser.getEditor().getItem().toString());
+        WMSCachedTileLoaderJob.THREAD_LIMIT.put((Integer) spinSimConn.getModel().getValue());
+        WMSLayer.PROP_IMAGE_SIZE.put((Integer) this.tileSize.getModel().getValue());
 
         return false;
     }
diff --git a/src/org/openstreetmap/josm/io/WMSLayerExporter.java b/src/org/openstreetmap/josm/io/WMSLayerExporter.java
index 4c8b1ed..15e765a 100644
--- a/src/org/openstreetmap/josm/io/WMSLayerExporter.java
+++ b/src/org/openstreetmap/josm/io/WMSLayerExporter.java
@@ -6,8 +6,11 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.ObjectOutputStream;
 
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
+import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 
 /**
@@ -17,6 +20,9 @@ import org.openstreetmap.josm.tools.CheckParameterUtil;
  */
 public class WMSLayerExporter extends FileExporter {
 
+    /** Which version of the file we export */
+    public static final int CURRENT_FILE_VERSION = 6;
+
     /**
      * Constructs a new {@code WMSLayerExporter}
      */
@@ -28,15 +34,20 @@ public class WMSLayerExporter extends FileExporter {
     public void exportData(File file, Layer layer) throws IOException {
         CheckParameterUtil.ensureParameterNotNull(file, "file");
         CheckParameterUtil.ensureParameterNotNull(layer, "layer");
-        if (layer instanceof WMSLayer) {
+
+        if (layer instanceof AbstractTileSourceLayer) {
             try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
-                ((WMSLayer)layer).writeExternal(oos);
+                oos.writeInt(CURRENT_FILE_VERSION); // file version
+                oos.writeObject(Main.map.mapView.getCenter());
+                ImageryPreferenceEntry entry = new ImageryPreferenceEntry(((AbstractTileSourceLayer) layer).getInfo());
+                oos.writeObject(Preferences.serializeStruct(entry, ImageryPreferenceEntry.class));
             }
         }
+
     }
 
     @Override
     public void activeLayerChange(Layer oldLayer, Layer newLayer) {
-        setEnabled(newLayer instanceof WMSLayer);
+        setEnabled(newLayer instanceof AbstractTileSourceLayer);
     }
 }
diff --git a/src/org/openstreetmap/josm/io/WMSLayerImporter.java b/src/org/openstreetmap/josm/io/WMSLayerImporter.java
index ea1daa3..df06db0 100644
--- a/src/org/openstreetmap/josm/io/WMSLayerImporter.java
+++ b/src/org/openstreetmap/josm/io/WMSLayerImporter.java
@@ -6,11 +6,17 @@ import static org.openstreetmap.josm.tools.I18n.tr;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InvalidClassException;
 import java.io.ObjectInputStream;
+import java.util.Map;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.ExtensionFileFilter;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -27,49 +33,66 @@ public class WMSLayerImporter extends FileImporter {
     public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter(
             "wms", "wms", tr("WMS Files (*.wms)"));
 
-    private final WMSLayer wmsLayer;
-
     /**
      * Constructs a new {@code WMSLayerImporter}.
      */
     public WMSLayerImporter() {
-        this(new WMSLayer());
-    }
-
-    /**
-     * Constructs a new {@code WMSLayerImporter} that will import data to the specified WMS layer.
-     * @param wmsLayer The WMS layer.
-     */
-    public WMSLayerImporter(WMSLayer wmsLayer) {
         super(FILE_FILTER);
-        this.wmsLayer = wmsLayer;
     }
 
+
     @Override
     public void importData(File file, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
         CheckParameterUtil.ensureParameterNotNull(file, "file");
+        final EastNorth zoomTo;
+        ImageryInfo info = null;
+        final ImageryLayer layer;
+
         try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
-            wmsLayer.readExternal(ois);
+            int sfv = ois.readInt();
+            if (sfv < 5) {
+                throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, 5));
+            } else if (sfv == 5) {
+                ois.readInt(); // dax - not needed
+                ois.readInt(); // day - not needed
+                zoomTo = null;
+
+                int imageSize = ois.readInt();
+                double pixelPerDegree = ois.readDouble();
+
+                String name = (String)ois.readObject();
+                String extendedUrl = (String)ois.readObject();
+
+                info = new ImageryInfo(name);
+                info.setExtendedUrl(extendedUrl);
+                info.setPixelPerDegree(pixelPerDegree);
+                info.setTileSize(imageSize);
+            } else if (sfv == WMSLayerExporter.CURRENT_FILE_VERSION){
+                zoomTo = (EastNorth) ois.readObject();
+
+                @SuppressWarnings("unchecked")
+                ImageryPreferenceEntry entry = Preferences.deserializeStruct(
+                        (Map<String, String>)ois.readObject(),
+                        ImageryPreferenceEntry.class);
+                info = new ImageryInfo(entry);
+            } else {
+                throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, 6));
+            }
         } catch (ClassNotFoundException e) {
             throw new IllegalDataException(e);
         }
+        layer = ImageryLayer.create(info);
+
 
         // FIXME: remove UI stuff from IO subsystem
         GuiHelper.runInEDT(new Runnable() {
             @Override
             public void run() {
-                Main.main.addLayer(wmsLayer);
-                wmsLayer.onPostLoadFromFile();
+                Main.main.addLayer(layer);
+                if (zoomTo != null) {
+                    Main.map.mapView.zoomTo(zoomTo);
                 }
-        });
             }
-
-    /**
-     * Replies the imported WMS layer.
-     * @return The imported WMS layer.
-     * @see #importData(File, ProgressMonitor)
-     */
-    public final WMSLayer getWmsLayer() {
-        return wmsLayer;
+        });
     }
 }
diff --git a/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java b/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java
deleted file mode 100644
index 591e8ef..0000000
--- a/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.io.imagery;
-
-import java.awt.image.BufferedImage;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.net.URL;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.StringTokenizer;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.data.preferences.StringProperty;
-import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Utils;
-
-public class HTMLGrabber extends WMSGrabber {
-    public static final StringProperty PROP_BROWSER = new StringProperty("imagery.wms.browser", "webkit-image {0}");
-
-    public HTMLGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
-        super(mv, layer, localOnly);
-    }
-
-    @Override
-    protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws IOException {
-        String urlstring = url.toExternalForm();
-
-        Main.info("Grabbing HTML " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);
-
-        List<String> cmdParams = new ArrayList<>();
-        StringTokenizer st = new StringTokenizer(MessageFormat.format(PROP_BROWSER.get(), urlstring));
-        while (st.hasMoreTokens()) {
-            cmdParams.add(st.nextToken());
-        }
-
-        ProcessBuilder builder = new ProcessBuilder( cmdParams);
-
-        Process browser;
-        try {
-            browser = builder.start();
-        } catch (IOException ioe) {
-            throw new IOException("Could not start browser. Please check that the executable path is correct.\n" + ioe.getMessage(), ioe);
-        }
-
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        Utils.copyStream(browser.getInputStream(), baos);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get()));
-        bais.reset();
-        layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
-
-        return img;
-    }
-}
diff --git a/src/org/openstreetmap/josm/io/imagery/WMSException.java b/src/org/openstreetmap/josm/io/imagery/WMSException.java
deleted file mode 100644
index 9018b12..0000000
--- a/src/org/openstreetmap/josm/io/imagery/WMSException.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.io.imagery;
-
-import java.net.URL;
-import java.util.Arrays;
-import java.util.Collection;
-
-import org.openstreetmap.josm.tools.Utils;
-
-/**
- * WMS Service Exception, as defined by {@code application/vnd.ogc.se_xml} format:<ul>
- * <li><a href="http://schemas.opengis.net/wms/1.1.0/exception_1_1_0.dtd">WMS 1.1.0 DTD</a></li>
- * <li><a href="http://schemas.opengis.net/wms/1.3.0/exception_1_3_0.dtd">WMS 1.3.0 XSD</a></li>
- * </ul>
- * @since 7425
- */
-public class WMSException extends Exception {
-
-    private final transient WMSRequest request;
-    private final URL url;
-    private final String[] exceptions;
-
-    /**
-     * Constructs a new {@code WMSException}.
-     * @param request the WMS request that lead to this exception
-     * @param url the URL that lead to this exception
-     * @param exceptions the exceptions replied by WMS server
-     */
-    public WMSException(WMSRequest request, URL url, Collection<String> exceptions) {
-        super(Utils.join("\n", exceptions));
-        this.request = request;
-        this.url = url;
-        this.exceptions = exceptions.toArray(new String[0]);
-    }
-
-    /**
-     * Replies the WMS request that lead to this exception.
-     * @return the WMS request
-     */
-    public final WMSRequest getRequest() {
-        return request;
-    }
-
-    /**
-     * Replies the URL that lead to this exception.
-     * @return the URL
-     */
-    public final URL getUrl() {
-        return url;
-    }
-
-    /**
-     * Replies the WMS Service exceptions.
-     * @return the exceptions
-     */
-    public final Collection<String> getExceptions() {
-        return Arrays.asList(exceptions);
-    }
-}
diff --git a/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java b/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java
deleted file mode 100644
index f35b0c9..0000000
--- a/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java
+++ /dev/null
@@ -1,312 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.io.imagery;
-
-import java.awt.image.BufferedImage;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.StringReader;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.nio.charset.StandardCharsets;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-import java.text.NumberFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.data.ProjectionBounds;
-import org.openstreetmap.josm.data.coor.EastNorth;
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.imagery.GeorefImage.State;
-import org.openstreetmap.josm.data.imagery.ImageryInfo;
-import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
-import org.openstreetmap.josm.io.OsmTransferException;
-import org.openstreetmap.josm.io.ProgressInputStream;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Utils;
-import org.w3c.dom.Document;
-import org.w3c.dom.NodeList;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-/**
- * WMS grabber, fetching tiles from WMS server.
- * @since 3715
- */
-public class WMSGrabber implements Runnable {
-
-    protected final MapView mv;
-    protected final WMSLayer layer;
-    private final boolean localOnly;
-
-    protected ProjectionBounds b;
-    protected volatile boolean canceled;
-
-    protected String baseURL;
-    private ImageryInfo info;
-    private Map<String, String> props = new HashMap<>();
-
-    /**
-     * Constructs a new {@code WMSGrabber}.
-     * @param mv Map view
-     * @param layer WMS layer
-     */
-    public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
-        this.mv = mv;
-        this.layer = layer;
-        this.localOnly = localOnly;
-        this.info = layer.getInfo();
-        this.baseURL = info.getUrl();
-        if (layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) {
-            props.put("Cookie", layer.getInfo().getCookies());
-        }
-        Pattern pattern = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
-        StringBuffer output = new StringBuffer();
-        Matcher matcher = pattern.matcher(this.baseURL);
-        while (matcher.find()) {
-            props.put(matcher.group(1),matcher.group(2));
-            matcher.appendReplacement(output, "");
-        }
-        matcher.appendTail(output);
-        this.baseURL = output.toString();
-    }
-
-    int width() {
-        return layer.getBaseImageWidth();
-    }
-
-    int height() {
-        return layer.getBaseImageHeight();
-    }
-
-    @Override
-    public void run() {
-        while (true) {
-            if (canceled)
-                return;
-            WMSRequest request = layer.getRequest(localOnly);
-            if (request == null)
-                return;
-            this.b = layer.getBounds(request);
-            if (request.isPrecacheOnly()) {
-                if (!layer.cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)) {
-                    attempt(request);
-                } else if (Main.isDebugEnabled()) {
-                    Main.debug("Ignoring "+request+" (precache only + exact match)");
-                }
-            } else if (!loadFromCache(request)){
-                attempt(request);
-            } else if (Main.isDebugEnabled()) {
-                Main.debug("Ignoring "+request+" (loaded from cache)");
-            }
-            layer.finishRequest(request);
-        }
-    }
-
-    protected void attempt(WMSRequest request){ // try to fetch the image
-        int maxTries = 5; // n tries for every image
-        for (int i = 1; i <= maxTries; i++) {
-            if (canceled)
-                return;
-            try {
-                if (!request.isPrecacheOnly() && !layer.requestIsVisible(request))
-                    return;
-                fetch(request, i);
-                break; // break out of the retry loop
-            } catch (IOException e) {
-                try { // sleep some time and then ask the server again
-                    Thread.sleep(random(1000, 2000));
-                } catch (InterruptedException e1) {
-                    Main.debug("InterruptedException in "+getClass().getSimpleName()+" during WMS request");
-                }
-                if (i == maxTries) {
-                    Main.error(e);
-                    request.finish(State.FAILED, null, null);
-                }
-            } catch (WMSException e) {
-                // Fail fast in case of WMS Service exception: useless to retry:
-                // either the URL is wrong or the server suffers huge problems
-                Main.error("WMS service exception while requesting "+e.getUrl()+":\n"+e.getMessage().trim());
-                request.finish(State.FAILED, null, e);
-                break; // break out of the retry loop
-            }
-        }
-    }
-
-    public static int random(int min, int max) {
-        return (int)(Math.random() * ((max+1)-min) ) + min;
-    }
-
-    public final void cancel() {
-        canceled = true;
-    }
-
-    private void fetch(WMSRequest request, int attempt) throws IOException, WMSException {
-        URL url = null;
-        try {
-            url = getURL(
-                    b.minEast, b.minNorth,
-                    b.maxEast, b.maxNorth,
-                    width(), height());
-            request.finish(State.IMAGE, grab(request, url, attempt), null);
-
-        } catch (IOException | OsmTransferException e) {
-            Main.error(e);
-            throw new IOException(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""), e);
-        }
-    }
-
-    public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
-
-    protected URL getURL(double w, double s,double e,double n,
-            int wi, int ht) throws MalformedURLException {
-        String myProj = Main.getProjection().toCode();
-        if (!info.getServerProjections().contains(myProj) && "EPSG:3857".equals(Main.getProjection().toCode())) {
-            LatLon sw = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));
-            LatLon ne = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));
-            myProj = "EPSG:4326";
-            s = sw.lat();
-            w = sw.lon();
-            n = ne.lat();
-            e = ne.lon();
-        }
-        if ("EPSG:4326".equals(myProj) && !info.getServerProjections().contains(myProj) && info.getServerProjections().contains("CRS:84")) {
-            myProj = "CRS:84";
-        }
-
-        // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
-        //
-        // Background:
-        //
-        // bbox=x_min,y_min,x_max,y_max
-        //
-        //      SRS=... is WMS 1.1.1
-        //      CRS=... is WMS 1.3.0
-        //
-        // The difference:
-        //      For SRS x is east-west and y is north-south
-        //      For CRS x and y are as specified by the EPSG
-        //          E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
-        //          For most other EPSG code there seems to be no difference.
-        // [1] https://www.epsg-registry.org/report.htm?type=selection&entity=urn:ogc:def:crs:EPSG::4326&reportDetail=short&style=urn:uuid:report-style:default-with-code&style_name=OGP%20Default%20With%20Code&title=EPSG:4326
-        boolean switchLatLon = false;
-        if (baseURL.toLowerCase(Locale.ENGLISH).contains("crs=epsg:4326")) {
-            switchLatLon = true;
-        } else if (baseURL.toLowerCase(Locale.ENGLISH).contains("crs=") && "EPSG:4326".equals(myProj)) {
-            switchLatLon = true;
-        }
-        String bbox;
-        if (switchLatLon) {
-            bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e));
-        } else {
-            bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n));
-        }
-        return new URL(baseURL.replaceAll("\\{proj(\\([^})]+\\))?\\}", myProj)
-                .replaceAll("\\{bbox\\}", bbox)
-                .replaceAll("\\{w\\}", latLonFormat.format(w))
-                .replaceAll("\\{s\\}", latLonFormat.format(s))
-                .replaceAll("\\{e\\}", latLonFormat.format(e))
-                .replaceAll("\\{n\\}", latLonFormat.format(n))
-                .replaceAll("\\{width\\}", String.valueOf(wi))
-                .replaceAll("\\{height\\}", String.valueOf(ht))
-                .replace(" ", "%20"));
-    }
-
-    public boolean loadFromCache(WMSRequest request) {
-        BufferedImage cached = layer.cache.getExactMatch(
-                Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
-
-        if (cached != null) {
-            request.finish(State.IMAGE, cached, null);
-            return true;
-        } else if (request.isAllowPartialCacheMatch()) {
-            BufferedImage partialMatch = layer.cache.getPartialMatch(
-                    Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
-            if (partialMatch != null) {
-                request.finish(State.PARTLY_IN_CACHE, partialMatch, null);
-                return true;
-            }
-        }
-
-        if (!request.isReal() && !layer.hasAutoDownload()){
-            request.finish(State.NOT_IN_CACHE, null, null);
-            return true;
-        }
-
-        return false;
-    }
-
-    protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws WMSException, IOException, OsmTransferException {
-        Main.info("Grabbing WMS " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);
-
-        HttpURLConnection conn = Utils.openHttpConnection(url);
-        conn.setUseCaches(true);
-        for (Entry<String, String> e : props.entrySet()) {
-            conn.setRequestProperty(e.getKey(), e.getValue());
-        }
-        conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15) * 1000);
-        conn.setReadTimeout(Main.pref.getInteger("socket.timeout.read", 30) * 1000);
-
-        String contentType = conn.getHeaderField("Content-Type");
-        if (conn.getResponseCode() != 200
-                || contentType != null && !contentType.startsWith("image") ) {
-            String xml = readException(conn);
-            try {
-                DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
-                InputSource is = new InputSource(new StringReader(xml));
-                Document doc = db.parse(is);
-                NodeList nodes = doc.getElementsByTagName("ServiceException");
-                List<String> exceptions = new ArrayList<>(nodes.getLength());
-                for (int i = 0; i < nodes.getLength(); i++) {
-                    exceptions.add(nodes.item(i).getTextContent());
-                }
-                throw new WMSException(request, url, exceptions);
-            } catch (SAXException | ParserConfigurationException ex) {
-                throw new IOException(xml, ex);
-            }
-        }
-
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        try (InputStream is = new ProgressInputStream(conn, null)) {
-            Utils.copyStream(is, baos);
-        }
-
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get()));
-        bais.reset();
-        layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
-        return img;
-    }
-
-    protected String readException(URLConnection conn) throws IOException {
-        StringBuilder exception = new StringBuilder();
-        InputStream in = conn.getInputStream();
-        try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
-            String line = null;
-            while( (line = br.readLine()) != null) {
-                // filter non-ASCII characters and control characters
-                exception.append(line.replaceAll("[^\\p{Print}]", ""));
-                exception.append('\n');
-            }
-            return exception.toString();
-        }
-    }
-}
diff --git a/src/org/openstreetmap/josm/io/imagery/WMSRequest.java b/src/org/openstreetmap/josm/io/imagery/WMSRequest.java
deleted file mode 100644
index db25eb1..0000000
--- a/src/org/openstreetmap/josm/io/imagery/WMSRequest.java
+++ /dev/null
@@ -1,154 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.io.imagery;
-
-import java.awt.image.BufferedImage;
-
-import org.openstreetmap.josm.data.imagery.GeorefImage.State;
-import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask;
-
-public class WMSRequest implements Comparable<WMSRequest> {
-    private final int xIndex;
-    private final int yIndex;
-    private final double pixelPerDegree;
-    private final boolean real; // Download even if autodownloading is disabled
-    private final PrecacheTask precacheTask; // Download even when wms tile is not currently visible (precache)
-    private final boolean allowPartialCacheMatch;
-    private int priority;
-    private boolean hasExactMatch;
-    // Result
-    private State state;
-    private BufferedImage image;
-    private WMSException exception;
-
-    public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch) {
-        this(xIndex, yIndex, pixelPerDegree, real, allowPartialCacheMatch, null);
-    }
-
-    public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch, PrecacheTask precacheTask) {
-        this.xIndex = xIndex;
-        this.yIndex = yIndex;
-        this.pixelPerDegree = pixelPerDegree;
-        this.real = real;
-        this.precacheTask = precacheTask;
-        this.allowPartialCacheMatch = allowPartialCacheMatch;
-    }
-
-    public void finish(State state, BufferedImage image, WMSException exception) {
-        this.state = state;
-        this.image = image;
-        this.exception = exception;
-    }
-
-    public int getXIndex() {
-        return xIndex;
-    }
-
-    public int getYIndex() {
-        return yIndex;
-    }
-
-    public double getPixelPerDegree() {
-        return pixelPerDegree;
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        long temp;
-        temp = Double.doubleToLongBits(pixelPerDegree);
-        result = prime * result + (int) (temp ^ (temp >>> 32));
-        result = prime * result + xIndex;
-        result = prime * result + yIndex;
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        WMSRequest other = (WMSRequest) obj;
-        if (Double.doubleToLongBits(pixelPerDegree) != Double
-                .doubleToLongBits(other.pixelPerDegree))
-            return false;
-        if (xIndex != other.xIndex)
-            return false;
-        if (yIndex != other.yIndex)
-            return false;
-        if (allowPartialCacheMatch != other.allowPartialCacheMatch)
-            return false;
-        return true;
-    }
-
-    public void setPriority(int priority) {
-        this.priority = priority;
-    }
-
-    public int getPriority() {
-        return priority;
-    }
-
-    @Override
-    public int compareTo(WMSRequest o) {
-        return priority - o.priority;
-    }
-
-    /**
-     * Replies the resulting state.
-     * @return the resulting state
-     */
-    public State getState() {
-        return state;
-    }
-
-    /**
-     * Replies the resulting image, if any.
-     * @return the resulting image, or {@code null}
-     */
-    public BufferedImage getImage() {
-        return image;
-    }
-
-    /**
-     * Replies the resulting exception, if any.
-     * @return the resulting exception, or {@code null}
-     * @since 7425
-     */
-    public WMSException getException() {
-        return exception;
-    }
-
-    @Override
-    public String toString() {
-        return "WMSRequest [xIndex=" + xIndex + ", yIndex=" + yIndex
-                + ", pixelPerDegree=" + pixelPerDegree + "]";
-    }
-
-    public boolean isReal() {
-        return real;
-    }
-
-    public boolean isPrecacheOnly() {
-        return precacheTask != null;
-    }
-
-    public PrecacheTask getPrecacheTask() {
-        return precacheTask;
-    }
-
-    public boolean isAllowPartialCacheMatch() {
-        return allowPartialCacheMatch;
-    }
-
-    public boolean hasExactMatch() {
-        return hasExactMatch;
-    }
-
-    public void setHasExactMatch(boolean hasExactMatch) {
-        this.hasExactMatch = hasExactMatch;
-    }
-}
diff --git a/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java b/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java
index da5ce92..ac621af 100644
--- a/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java
+++ b/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java
@@ -16,6 +16,7 @@ import javax.swing.SwingConstants;
 
 import org.openstreetmap.josm.data.Preferences;
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
+import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.TMSLayer;
@@ -79,10 +80,11 @@ public class ImagerySessionExporter implements SessionLayerExporter {
         layerElem.setAttribute("version", "0.1");
         ImageryPreferenceEntry e = new ImageryPreferenceEntry(layer.getInfo());
         Map<String,String> data = new LinkedHashMap<>(Preferences.serializeStruct(e, ImageryPreferenceEntry.class));
-        if (layer instanceof WMSLayer) {
-            WMSLayer wms = (WMSLayer) layer;
-            data.put("automatic-downloading", Boolean.toString(wms.hasAutoDownload()));
-            data.put("automatically-change-resolution", Boolean.toString(wms.isAutoResolution()));
+        if (layer instanceof AbstractTileSourceLayer) {
+            AbstractTileSourceLayer tsLayer = (AbstractTileSourceLayer) layer;
+            data.put("automatic-downloading", Boolean.toString(tsLayer.autoLoad));
+            data.put("automatically-change-resolution", Boolean.toString(tsLayer.autoZoom));
+            data.put("show-errors", Boolean.toString(tsLayer.showErrors));
         }
         for (Map.Entry<String,String> entry : data.entrySet()) {
             Element attrElem = support.createElement(entry.getKey());
diff --git a/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java b/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java
index 95d48ec..3f7346d 100644
--- a/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java
+++ b/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java
@@ -10,9 +10,9 @@ import java.util.Map;
 import org.openstreetmap.josm.data.Preferences;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
+import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
-import org.openstreetmap.josm.gui.layer.WMSLayer;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
 import org.openstreetmap.josm.io.IllegalDataException;
 import org.openstreetmap.josm.io.session.SessionReader.ImportSupport;
@@ -45,15 +45,18 @@ public class ImagerySessionImporter implements SessionLayerImporter {
         ImageryPreferenceEntry prefEntry = Preferences.deserializeStruct(attributes, ImageryPreferenceEntry.class);
         ImageryInfo i = new ImageryInfo(prefEntry);
         ImageryLayer layer = ImageryLayer.create(i);
-        if (layer instanceof WMSLayer) {
-            WMSLayer wms = (WMSLayer) layer;
-            String autoDownload = attributes.get("automatic-downloading");
-            if (autoDownload != null) {
-                wms.setAutoDownload(Boolean.parseBoolean(autoDownload));
-            }
-            String autoResolution = attributes.get("automatically-change-resolution");
-            if (autoResolution != null) {
-                wms.setAutoResolution(Boolean.parseBoolean(autoResolution));
+        if (layer instanceof AbstractTileSourceLayer) {
+            AbstractTileSourceLayer tsLayer = (AbstractTileSourceLayer) layer;
+            if (attributes.containsKey("automatic-downloading")) {
+                tsLayer.autoLoad = new Boolean(attributes.get("automatic-downloading")).booleanValue();
+            }
+
+            if (attributes.containsKey("automatically-change-resolution")) {
+                tsLayer.autoZoom = new Boolean(attributes.get("automatically-change-resolution")).booleanValue();
+            }
+
+            if (attributes.containsKey("show-errors")) {
+                tsLayer.showErrors = new Boolean(attributes.get("show-errors")).booleanValue();
             }
         }
         return layer;
