Index: trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java	(revision 13733)
@@ -13,7 +13,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.swing.JComboBox;
@@ -27,4 +26,5 @@
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
 import org.openstreetmap.josm.data.imagery.WMTSTileSource;
+import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.layer.AlignImageryPanel;
@@ -34,5 +34,4 @@
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.imagery.WMSImagery;
-import org.openstreetmap.josm.io.imagery.WMSImagery.LayerDetails;
 import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -96,17 +95,24 @@
             case WMS_ENDPOINT:
                 // convert to WMS type
-                return getWMSLayerInfo(info);
+                if (info.getDefaultLayers() == null || info.getDefaultLayers().isEmpty()) {
+                    return getWMSLayerInfo(info);
+                } else {
+                    return info;
+                }
             case WMTS:
                 // specify which layer to use
-                DefaultLayer layerId = new WMTSTileSource(info).userSelectLayer();
-                if (layerId != null) {
-                    ImageryInfo copy = new ImageryInfo(info);
-                    Collection<DefaultLayer> defaultLayers = new ArrayList<>(1);
-                    defaultLayers.add(layerId);
-                    copy.setDefaultLayers(defaultLayers);
-                    return copy;
-                }
-                // layer not selected - refuse to add
-                return null;
+                if (info.getDefaultLayers() == null || info.getDefaultLayers().isEmpty()) {
+                    DefaultLayer layerId = new WMTSTileSource(info).userSelectLayer();
+                    if (layerId != null) {
+                        ImageryInfo copy = new ImageryInfo(info);
+                        List<DefaultLayer> defaultLayers = new ArrayList<>(1);
+                        defaultLayers.add(layerId);
+                        copy.setDefaultLayers(defaultLayers);
+                        return copy;
+                    }
+                    return null;
+                } else {
+                    return info;
+                }
             default:
                 return info;
@@ -130,4 +136,10 @@
             }
             Logging.log(Logging.LEVEL_ERROR, "Could not parse WMS layer list. Incoming data:\n"+ex.getIncomingData(), ex);
+        } catch (WMTSGetCapabilitiesException e) {
+            if (!GraphicsEnvironment.isHeadless()) {
+                JOptionPane.showMessageDialog(Main.parent, tr("Could not parse WMTS layer list."),
+                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+            }
+            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMTS layer list.", e);
         }
         return null;
@@ -166,45 +178,68 @@
      */
     protected static ImageryInfo getWMSLayerInfo(ImageryInfo info) throws IOException, WMSGetCapabilitiesException {
-        CheckParameterUtil.ensureThat(ImageryType.WMS_ENDPOINT.equals(info.getImageryType()), "wms_endpoint imagery type expected");
-
-        final WMSImagery wms = new WMSImagery();
-        wms.attemptGetCapabilities(info.getUrl());
-
-        final WMSLayerTree tree = new WMSLayerTree();
-        tree.updateTree(wms);
-        List<String> wmsFormats = wms.getFormats();
-        final JComboBox<String> formats = new JComboBox<>(wmsFormats.toArray(new String[0]));
-        formats.setSelectedItem(wms.getPreferredFormats());
-        formats.setToolTipText(tr("Select image format for WMS layer"));
-
-        if (!GraphicsEnvironment.isHeadless() && 1 != new SelectWmsLayersDialog(tree, formats).showDialog().getValue()) {
-            return null;
-        }
-
-        final String url = wms.buildGetMapUrl(
-                tree.getSelectedLayers(), (String) formats.getSelectedItem());
-        Set<String> supportedCrs = new HashSet<>();
-        boolean first = true;
-        StringBuilder layersString = new StringBuilder();
-        for (LayerDetails layer: tree.getSelectedLayers()) {
-            if (first) {
-                supportedCrs.addAll(layer.getProjections());
-                first = false;
-            }
-            layersString.append(layer.name);
-            layersString.append(", ");
-            supportedCrs.retainAll(layer.getProjections());
-        }
-
-        // copy all information from WMS
-        ImageryInfo ret = new ImageryInfo(info);
-        // and update according to user choice
-        ret.setUrl(url);
-        ret.setImageryType(ImageryType.WMS);
-        if (layersString.length() > 2) {
-            ret.setName(ret.getName() + ' ' + layersString.substring(0, layersString.length() - 2));
-        }
-        ret.setServerProjections(supportedCrs);
-        return ret;
+        try {
+            CheckParameterUtil.ensureThat(ImageryType.WMS_ENDPOINT.equals(info.getImageryType()), "wms_endpoint imagery type expected");
+            final WMSImagery wms = new WMSImagery(info.getUrl());
+
+            final WMSLayerTree tree = new WMSLayerTree();
+            tree.updateTree(wms);
+
+            Collection<String> wmsFormats = wms.getFormats();
+            final JComboBox<String> formats = new JComboBox<>(wmsFormats.toArray(new String[wmsFormats.size()]));
+            formats.setSelectedItem(wms.getPreferredFormat());
+            formats.setToolTipText(tr("Select image format for WMS layer"));
+
+            if (!GraphicsEnvironment.isHeadless()) {
+                if (1 != new ExtendedDialog(Main.parent, tr("Select WMS layers"), new String[]{tr("Add layers"), tr("Cancel")}) { {
+                    final JScrollPane scrollPane = new JScrollPane(tree.getLayerTree());
+                    scrollPane.setPreferredSize(new Dimension(400, 400));
+                    final JPanel panel = new JPanel(new GridBagLayout());
+                    panel.add(scrollPane, GBC.eol().fill());
+                    panel.add(formats, GBC.eol().fill(GBC.HORIZONTAL));
+                    setContent(panel);
+                } }.showDialog().getValue()) {
+                    return null;
+                }
+            }
+
+            final String url = wms.buildGetMapUrl(
+                    tree.getSelectedLayers().stream().map(x -> x.getName()).collect(Collectors.toList()),
+                    (List<String>) null,
+                    (String) formats.getSelectedItem(),
+                    true // TODO: ask the user if (s)he wants transparent layer
+                    );
+
+            String selectedLayers = tree.getSelectedLayers().stream()
+                    .map(x -> x.getName())
+                    .collect(Collectors.joining(", "));
+            ImageryInfo ret = new ImageryInfo(info.getName() + selectedLayers,
+                    url,
+                    "wms",
+                    info.getEulaAcceptanceRequired(),
+                    info.getCookies());
+
+            ret.setServerProjections(wms.getServerProjections(tree.getSelectedLayers()));
+
+            return ret;
+        } catch (MalformedURLException ex) {
+            if (!GraphicsEnvironment.isHeadless()) {
+                JOptionPane.showMessageDialog(Main.parent, tr("Invalid service URL."),
+                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+            }
+            Logging.log(Logging.LEVEL_ERROR, ex);
+        } catch (IOException ex) {
+            if (!GraphicsEnvironment.isHeadless()) {
+                JOptionPane.showMessageDialog(Main.parent, tr("Could not retrieve WMS layer list."),
+                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+            }
+            Logging.log(Logging.LEVEL_ERROR, ex);
+        } catch (WMSGetCapabilitiesException ex) {
+            if (!GraphicsEnvironment.isHeadless()) {
+                JOptionPane.showMessageDialog(Main.parent, tr("Could not parse WMS layer list."),
+                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+            }
+            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMS layer list. Incoming data:\n"+ex.getIncomingData(), ex);
+        }
+        return null;
     }
 
Index: trunk/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java	(revision 13733)
@@ -21,4 +21,5 @@
 import org.apache.commons.jcs.engine.behavior.ICacheElement;
 import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
+import org.openstreetmap.josm.data.imagery.TileJobOptions;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -93,36 +94,31 @@
     private Runnable finishTask;
     private boolean force;
+    private long minimumExpiryTime;
 
     /**
      * @param cache cache instance that we will work on
-     * @param headers HTTP headers to be sent together with request
-     * @param readTimeout when connecting to remote resource
-     * @param connectTimeout when connecting to remote resource
+     * @param options options of the request
      * @param downloadJobExecutor that will be executing the jobs
      */
     public JCSCachedTileLoaderJob(ICacheAccess<K, V> cache,
-            int connectTimeout, int readTimeout,
-            Map<String, String> headers,
+            TileJobOptions options,
             ThreadPoolExecutor downloadJobExecutor) {
         CheckParameterUtil.ensureParameterNotNull(cache, "cache");
         this.cache = cache;
         this.now = System.currentTimeMillis();
-        this.connectTimeout = connectTimeout;
-        this.readTimeout = readTimeout;
-        this.headers = headers;
+        this.connectTimeout = options.getConnectionTimeout();
+        this.readTimeout = options.getReadTimeout();
+        this.headers = options.getHeaders();
         this.downloadJobExecutor = downloadJobExecutor;
+        this.minimumExpiryTime = TimeUnit.SECONDS.toMillis(options.getMinimumExpiryTime());
     }
 
     /**
      * @param cache cache instance that we will work on
-     * @param headers HTTP headers to be sent together with request
-     * @param readTimeout when connecting to remote resource
-     * @param connectTimeout when connecting to remote resource
+     * @param options of the request
      */
     public JCSCachedTileLoaderJob(ICacheAccess<K, V> cache,
-            int connectTimeout, int readTimeout,
-            Map<String, String> headers) {
-        this(cache, connectTimeout, readTimeout,
-                headers, DEFAULT_DOWNLOAD_JOB_DISPATCHER);
+            TileJobOptions options) {
+        this(cache, options, DEFAULT_DOWNLOAD_JOB_DISPATCHER);
     }
 
@@ -278,5 +274,5 @@
             // put a limit to the expire time (some servers send a value
             // that is too large)
-            expires = Math.min(expires, attributes.getCreateTime() + EXPIRE_TIME_SERVER_LIMIT);
+            expires = Math.min(expires, attributes.getCreateTime() + Math.max(EXPIRE_TIME_SERVER_LIMIT, minimumExpiryTime));
             if (now > expires) {
                 Logging.debug("JCS - Object {0} has expired -> valid to {1}, now is: {2}",
@@ -285,9 +281,9 @@
             }
         } else if (attributes.getLastModification() > 0 &&
-                now - attributes.getLastModification() > DEFAULT_EXPIRE_TIME) {
+                now - attributes.getLastModification() > Math.max(DEFAULT_EXPIRE_TIME, minimumExpiryTime)) {
             // check by file modification date
             Logging.debug("JCS - Object has expired, maximum file age reached {0}", getUrlNoException());
             return false;
-        } else if (now - attributes.getCreateTime() > DEFAULT_EXPIRE_TIME) {
+        } else if (now - attributes.getCreateTime() > Math.max(DEFAULT_EXPIRE_TIME, minimumExpiryTime)) {
             Logging.debug("JCS - Object has expired, maximum time since object creation reached {0}", getUrlNoException());
             return false;
@@ -330,4 +326,7 @@
                 // and the server answers with a HTTP 304 = "Not Modified"
                 Logging.debug("JCS - If-Modified-Since/ETag test: local version is up to date: {0}", getUrl());
+                // update cache attributes
+                attributes = parseHeaders(urlConn);
+                cache.put(getCacheKey(), cacheData, attributes);
                 return true;
             } else if (isObjectLoadable() // we have an object in cache, but we haven't received 304 response code
@@ -453,7 +452,10 @@
                 Logging.trace(e);
             }
-        }
-
-        ret.setExpirationTime(lng);
+            if (lng.equals(0L)) {
+                lng = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;
+            }
+        }
+
+        ret.setExpirationTime(Math.max(minimumExpiryTime + System.currentTimeMillis(), lng));
         ret.setLastModification(now);
         ret.setEtag(urlConn.getHeaderField("ETag"));
@@ -480,6 +482,12 @@
         final HttpClient.Response urlConn = getRequest("HEAD", false).connect();
         long lastModified = urlConn.getLastModified();
-        return (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getHeaderField("ETag"))) ||
+        boolean ret = (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getHeaderField("ETag"))) ||
                 (lastModified != 0 && lastModified <= attributes.getLastModification());
+        if (ret) {
+            // update attributes
+            attributes = parseHeaders(urlConn);
+            cache.put(getCacheKey(), cacheData, attributes);
+        }
+        return ret;
     }
 
Index: trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java	(revision 13733)
@@ -3,4 +3,8 @@
 
 import java.awt.Point;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
 
 import org.openstreetmap.gui.jmapviewer.Projected;
@@ -24,4 +28,6 @@
 public abstract class AbstractWMSTileSource extends TMSTileSource {
 
+    static final NumberFormat LATLON_FORMAT = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
+
     private EastNorth anchorPosition;
     private int[] tileXMin;
@@ -209,3 +215,24 @@
         return this.tileProjection.toCode();
     }
+
+    protected String getBbox(int zoom, int tilex, int tiley, boolean switchLatLon) {
+        EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
+        EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
+
+        double w = nw.getX();
+        double n = nw.getY();
+
+        double s = se.getY();
+        double e = se.getX();
+
+        return (
+                switchLatLon ?
+                        String.format("%s,%s,%s,%s",
+                                LATLON_FORMAT.format(s), LATLON_FORMAT.format(w), LATLON_FORMAT.format(n), LATLON_FORMAT.format(e))
+                        :
+                        String.format("%s,%s,%s,%s",
+                                LATLON_FORMAT.format(w), LATLON_FORMAT.format(s), LATLON_FORMAT.format(e), LATLON_FORMAT.format(n))
+
+                );
+    }
 }
Index: trunk/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java	(revision 13733)
@@ -44,7 +44,6 @@
                     TileLoaderListener.class,
                     ICacheAccess.class,
-                    int.class,
-                    int.class,
-                    Map.class);
+                    TileJobOptions.class
+                    );
         } catch (NoSuchMethodException | SecurityException e) {
             Logging.log(Logging.LEVEL_WARN, "Unable to initialize cache tile loader factory", e);
@@ -64,5 +63,5 @@
 
     @Override
-    public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
+    public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders, long minimumExpiryTime) {
         Map<String, String> headers = new ConcurrentHashMap<>();
         headers.put("User-Agent", Version.getInstance().getFullAgentString());
@@ -72,18 +71,21 @@
 
         return getLoader(listener, cache,
-                (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.connect", 15)),
-                (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.read", 30)),
-                headers);
+                new TileJobOptions(
+                        (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.connect", 15)),
+                        (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.read", 30)),
+                        headers,
+                        minimumExpiryTime
+                        )
+                );
     }
 
     protected TileLoader getLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
-            int connectTimeout, int readTimeout, Map<String, String> headers) {
+            TileJobOptions options) {
         try {
             return tileLoaderConstructor.newInstance(
                     listener,
                     cache,
-                    connectTimeout,
-                    readTimeout,
-                    headers);
+                    options
+                    );
         } catch (IllegalArgumentException e) {
             Logging.warn(e);
Index: trunk/src/org/openstreetmap/josm/data/imagery/DefaultLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/DefaultLayer.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/DefaultLayer.java	(revision 13733)
@@ -1,4 +1,12 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
 
 /**
@@ -12,13 +20,22 @@
  */
 public class DefaultLayer {
-
-    protected String layerName;
+    private final String layerName;
+    private final String tileMatrixSet;
+    private final String style;
 
     /**
      * Constructor
-     * @param layerName that is the DefaultLayer
+     * @param imageryType for which this layer is defined
+     * @param layerName as returned by getIdentifier for WMTS and getName for WMS
+     * @param style of the layer
+     * @param tileMatrixSet only for WMTS - tileMatrixSet to use
      */
-    public DefaultLayer(String layerName) {
-        this.layerName = layerName;
+    public DefaultLayer(ImageryType imageryType, String layerName, String style, String tileMatrixSet) {
+        this.layerName = layerName == null ? "" : layerName;
+        this.style = style == null ? "" : style;
+        if (!imageryType.equals(ImageryType.WMTS) && !(tileMatrixSet == null || "".equals(tileMatrixSet))) {
+            throw new IllegalArgumentException(tr("{0} imagery has tileMatrixSet defined to: {1}", imageryType, tileMatrixSet));
+        }
+        this.tileMatrixSet = tileMatrixSet == null ? "" : tileMatrixSet;
     }
 
@@ -30,3 +47,37 @@
     }
 
+    /**
+     * @return default tileMatrixSet. Only usable for WMTS
+     */
+    public String getTileMatrixSet() {
+        return tileMatrixSet;
+    }
+
+    /**
+     * @return style for this WMS / WMTS layer to use
+     */
+    public String getStyle() {
+        return style;
+    }
+
+    /**
+     * @return JSON representation of the default layer object
+     */
+    public JsonObject toJson() {
+        JsonObjectBuilder ret = Json.createObjectBuilder();
+        ret.add("layerName", layerName);
+        ret.add("style", style);
+        ret.add("tileMatrixSet", tileMatrixSet);
+        return ret.build();
+    }
+
+    /**
+     * Factory method creating DefaultLayer from JSON objects
+     * @param o serialized DefaultLayer object
+     * @param type of ImageryType serialized
+     * @return DefaultLayer instance based on JSON object
+     */
+    public static DefaultLayer fromJson(JsonObject o, ImageryType type) {
+        return new DefaultLayer(type, o.getString("layerName"), o.getString("style"), o.getString("tileMatrixSet"));
+    }
 }
Index: trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java	(revision 13733)
@@ -5,4 +5,5 @@
 
 import java.awt.Image;
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -15,8 +16,13 @@
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.stream.JsonCollectors;
 import javax.swing.ImageIcon;
 
@@ -218,7 +224,17 @@
     private boolean isGeoreferenceValid;
     /** which layers should be activated by default on layer addition. **/
-    private Collection<DefaultLayer> defaultLayers = Collections.emptyList();
-    // when adding a field, also adapt the ImageryInfo(ImageryInfo)
-    // and ImageryInfo(ImageryPreferenceEntry) constructor, equals method, and ImageryPreferenceEntry
+    private List<DefaultLayer> defaultLayers = new ArrayList<>();
+    /** HTTP headers **/
+    private Map<String, String> customHttpHeaders = new ConcurrentHashMap<>();
+    /** Should this map be transparent **/
+    private boolean transparent = true;
+    private int minimumTileExpire = (int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get());
+    /** when adding a field, also adapt the:
+     * {@link #ImageryPreferenceEntry ImageryPreferenceEntry object}
+     * {@link #ImageryPreferenceEntry#ImageryPreferenceEntry(ImageryInfo) ImageryPreferenceEntry constructor}
+     * {@link #ImageryInfo(ImageryPreferenceEntry) ImageryInfo constructor}
+     * {@link #ImageryInfo(ImageryInfo) ImageryInfo constructor}
+     * {@link #equalsPref(ImageryPreferenceEntry) equalsPref method}
+     **/
 
     /**
@@ -258,6 +274,8 @@
         @StructEntry boolean modTileFeatures;
         @StructEntry boolean overlay;
-        // TODO: disabled until change of layers is implemented
-        // @StructEntry String default_layers;
+        @StructEntry String default_layers;
+        @StructEntry Map<String, String> customHttpHeaders;
+        @StructEntry boolean transparent;
+        @StructEntry int minimumTileExpire;
 
         /**
@@ -308,5 +326,7 @@
                 }
             }
-            projections = i.serverProjections.stream().collect(Collectors.joining(","));
+            if (!i.serverProjections.isEmpty()) {
+                projections = i.serverProjections.stream().collect(Collectors.joining(","));
+            }
             if (i.noTileHeaders != null && !i.noTileHeaders.isEmpty()) {
                 noTileHeaders = new MultiMap<>(i.noTileHeaders);
@@ -325,6 +345,10 @@
             valid_georeference = i.isGeoreferenceValid();
             modTileFeatures = i.isModTileFeatures();
-            // TODO disabled until change of layers is implemented
-            // default_layers = i.defaultLayers.stream().collect(Collectors.joining(","));
+            if (!i.defaultLayers.isEmpty()) {
+                default_layers = i.defaultLayers.stream().map(x -> x.toJson()).collect(JsonCollectors.toJsonArray()).toString();
+            }
+            customHttpHeaders = i.customHttpHeaders;
+            transparent = i.isTransparent();
+            minimumTileExpire = i.minimumTileExpire;
         }
 
@@ -468,6 +492,14 @@
         isGeoreferenceValid = e.valid_georeference;
         modTileFeatures = e.modTileFeatures;
-        // TODO disabled until change of layers is implemented
-        // defaultLayers = Arrays.asList(e.default_layers.split(","));
+        if (e.default_layers != null) {
+            defaultLayers = Json.createReader(new StringReader(e.default_layers)).
+                    readArray().
+                    stream().
+                    map(x -> DefaultLayer.fromJson((JsonObject) x, imageryType)).
+                    collect(Collectors.toList());
+        }
+        customHttpHeaders = e.customHttpHeaders;
+        transparent = e.transparent;
+        minimumTileExpire = e.minimumTileExpire;
     }
 
@@ -514,4 +546,7 @@
         this.isGeoreferenceValid = i.isGeoreferenceValid;
         this.defaultLayers = i.defaultLayers;
+        this.customHttpHeaders = i.customHttpHeaders;
+        this.transparent = i.transparent;
+        this.minimumTileExpire = i.minimumTileExpire;
     }
 
@@ -565,5 +600,8 @@
                 Objects.equals(this.noTileChecksums, other.noTileChecksums) &&
                 Objects.equals(this.metadataHeaders, other.metadataHeaders) &&
-                Objects.equals(this.defaultLayers, other.defaultLayers);
+                Objects.equals(this.defaultLayers, other.defaultLayers) &&
+                Objects.equals(this.customHttpHeaders, other.customHttpHeaders) &&
+                Objects.equals(this.transparent, other.transparent) &&
+                Objects.equals(this.minimumTileExpire, other.minimumTileExpire);
         // CHECKSTYLE.ON: BooleanExpressionComplexity
     }
@@ -1372,5 +1410,5 @@
      * @return Collection of the layer names
      */
-    public Collection<DefaultLayer> getDefaultLayers() {
+    public List<DefaultLayer> getDefaultLayers() {
         return defaultLayers;
     }
@@ -1380,11 +1418,53 @@
      * @param layers set the list of default layers
      */
-    public void setDefaultLayers(Collection<DefaultLayer> layers) {
-        if (ImageryType.WMTS.equals(this.imageryType)) {
-            CheckParameterUtil.ensureThat(layers == null ||
-                    layers.isEmpty() ||
-                    layers.iterator().next() instanceof WMTSDefaultLayer, "Incorrect default layer");
-        }
+    public void setDefaultLayers(List<DefaultLayer> layers) {
         this.defaultLayers = layers;
     }
+
+    /**
+     * Returns custom HTTP headers that should be sent with request towards imagery provider
+     * @return headers
+     */
+    public Map<String, String> getCustomHttpHeaders() {
+        return customHttpHeaders;
+    }
+
+    /**
+     * Sets custom HTTP headers that should be sent with request towards imagery provider
+     * @param customHttpHeaders
+     */
+    public void setCustomHttpHeaders(Map<String, String> customHttpHeaders) {
+        this.customHttpHeaders = customHttpHeaders;
+    }
+
+    /**
+     * @return should this imagery be transparent
+     */
+    public boolean isTransparent() {
+        return transparent;
+    }
+
+    /**
+     *
+     * @param transparent set to true if imagery should be transparent
+     */
+    public void setTransparent(boolean transparent) {
+        this.transparent = transparent;
+    }
+
+    /**
+     * @return minimum tile expiration in seconds
+     */
+    public int getMinimumTileExpire() {
+        return minimumTileExpire;
+    }
+
+    /**
+     * Sets minimum tile expiration in seconds
+     * @param minimumTileExpire
+     */
+    public void setMinimumTileExpire(int minimumTileExpire) {
+        this.minimumTileExpire = minimumTileExpire;
+
+    }
 }
Index: trunk/src/org/openstreetmap/josm/data/imagery/LayerDetails.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/LayerDetails.java	(revision 13733)
+++ trunk/src/org/openstreetmap/josm/data/imagery/LayerDetails.java	(revision 13733)
@@ -0,0 +1,209 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
+
+import org.openstreetmap.josm.data.Bounds;
+
+/**
+ * The details of a layer of this WMS server.
+ */
+public class LayerDetails {
+    private Map<String, String> styles = new ConcurrentHashMap<>(); // name -> title
+    private Collection<String> crs = new ArrayList<>();
+    /**
+     * The layer name (WMS {@code Title})
+     */
+    private String title;
+    /**
+     * The layer name (WMS {@code Name})
+     */
+    private String name;
+    /**
+     * The layer abstract (WMS {@code Abstract})
+     * @since 13199
+     */
+    private String abstr;
+    private LayerDetails parentLayer;
+    private Bounds bounds;
+    private List<LayerDetails> children = new ArrayList<>();
+
+    /**
+     * Constructor pointing to parent layer. Set to null if this is topmost layer.
+     * This is needed to properly handle layer attributes inheritance.
+     *
+     * @param parentLayer
+     */
+    public LayerDetails(LayerDetails parentLayer) {
+        this.parentLayer = parentLayer;
+    }
+
+    /**
+     * @return projections that are supported by this layer
+     */
+    public Collection<String> getCrs() {
+        Collection<String> ret = new ArrayList<>();
+        if (parentLayer != null) {
+            ret.addAll(parentLayer.getCrs());
+        }
+        ret.addAll(crs);
+        return crs;
+    }
+
+    /**
+     *
+     * @return styles defined for this layer
+     */
+    public Map<String, String> getStyles() {
+        Map<String, String> ret = new ConcurrentHashMap<>();
+        if (parentLayer != null) {
+            ret.putAll(parentLayer.getStyles());
+        }
+        ret.putAll(styles);
+        return ret;
+    }
+
+    /**
+     * @see LayerDetails#getName()
+     * @return title "Human readable" title of this layer
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * @see LayerDetails#getName()
+     * @param title set title of this layer
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    /**
+     *
+     * Citation from OGC WMS specification (WMS 1.3.0):
+     * > A number of elements have both a <Name> and a <Title>. The Name is a text string used for machine-to-machine
+     * > communication while the Title is for the benefit of humans. For example, a dataset might have the descriptive Title
+     * > “Maximum Atmospheric Temperature” and be requested using the abbreviated Name “ATMAX”.
+     *
+     * And second citation:
+     * > If, and only if, a layer has a <Name>, then it is a map layer that can be requested by using that Name in the
+     * > LAYERS parameter of a GetMap request. A Layer that contains a <Name> element is referred to as a “named
+     * > layer” in this International Standard. If the layer has a Title but no Name, then that layer is only a category title for
+     * > all the layers nested within.
+     * @return name of this layer
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @see LayerDetails#getName()
+     * @param name sets the name of this Layer
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Add style to list of styles defined by this layer
+     * @param name machine-to-machine name of this style
+     * @param title human readable title of this style
+     */
+    public void addStyle(String name, String title) {
+        this.styles.put(name, title);
+    }
+
+    /**
+     * Add projection supported by this layer
+     * @param crs projection code
+     */
+    public void addCrs(String crs) {
+        this.crs.add(crs);
+    }
+
+    /**
+     *
+     * @return bounds within layer might be queried
+     */
+    public Bounds getBounds() {
+        return bounds;
+    }
+
+    /**
+     * sets bounds of this layer
+     * @param bounds
+     */
+    public void setBounds(Bounds bounds) {
+        this.bounds = bounds;
+    }
+
+    @Override
+    public String toString() {
+        String baseName = (title == null || title.isEmpty()) ? name : title;
+        return abstr == null || abstr.equalsIgnoreCase(baseName) ? baseName : baseName + " (" + abstr + ')';
+    }
+
+    /**
+     *
+     * @return parent layer for his layer
+     */
+    public LayerDetails getParent() {
+        return parentLayer;
+    }
+
+    /**
+     * sets children layers for this layer
+     * @param children
+     */
+    public void setChildren(List<LayerDetails> children) {
+        this.children = children;
+
+    }
+
+    /**
+     *
+     * @return children layers of this layer
+     */
+    public List<LayerDetails> getChildren() {
+        return children;
+    }
+
+    /**
+     * if user may select this layer (is it possible to request it from server)
+     * @return true if user may select this layer, false if this layer is only grouping other layers
+     */
+    public boolean isSelectable() {
+        return !(name == null || name.isEmpty());
+    }
+
+    /**
+     * @return "Narrative description of the layer"
+     */
+    public String getAbstract() {
+        return abstr;
+    }
+
+    /**
+     * Sets abstract of this layer
+     * @param abstr
+     */
+    public void setAbstract(String abstr) {
+        this.abstr = abstr;
+    }
+
+    /**
+     * @return flattened stream of this layer and its children (as well as recursively children of its children)
+     */
+    public Stream<LayerDetails> flattened() {
+        return Stream.concat(
+                Stream.of(this),
+                getChildren().stream().flatMap(LayerDetails::flattened)
+                );
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java	(revision 13733)
@@ -2,5 +2,4 @@
 package org.openstreetmap.josm.data.imagery;
 
-import java.util.Map;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -27,7 +26,4 @@
 
     protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
-    protected final int connectTimeout;
-    protected final int readTimeout;
-    protected final Map<String, String> headers;
     protected final TileLoaderListener listener;
 
@@ -50,20 +46,17 @@
 
     private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER;
+    protected final TileJobOptions options;
 
     /**
      * Constructor
      * @param listener          called when tile loading has finished
-     * @param cache              of the cache
-     * @param connectTimeout    to remote resource
-     * @param readTimeout       to remote resource
-     * @param headers           HTTP headers to be sent along with request
+     * @param cache             of the cache
+     * @param options           tile job options
      */
     public TMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
-            int connectTimeout, int readTimeout, Map<String, String> headers) {
+           TileJobOptions options) {
         CheckParameterUtil.ensureParameterNotNull(cache, "cache");
         this.cache = cache;
-        this.connectTimeout = connectTimeout;
-        this.readTimeout = readTimeout;
-        this.headers = headers;
+        this.options = options;
         this.listener = listener;
     }
@@ -98,6 +91,10 @@
     @Override
     public TileJob createTileLoaderJob(Tile tile) {
-        return new TMSCachedTileLoaderJob(listener, tile, cache,
-                connectTimeout, readTimeout, headers, getDownloadExecutor());
+        return new TMSCachedTileLoaderJob(
+                listener,
+                tile,
+                cache,
+                options,
+                getDownloadExecutor());
     }
 
Index: trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java	(revision 13733)
@@ -44,9 +44,12 @@
  */
 public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener {
-    private static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
-    private static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
+    /** General maximum expires for tiles. Might be overridden by imagery settings */
+    public static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
+    /** General minimum expires for tiles. Might be overridden by imagery settings */
+    public static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
     static final Pattern SERVICE_EXCEPTION_PATTERN = Pattern.compile("(?s).+<ServiceException[^>]*>(.+)</ServiceException>.+");
     protected final Tile tile;
     private volatile URL url;
+    private final TileJobOptions options;
 
     // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
@@ -59,15 +62,14 @@
      * @param tile to be fetched from cache
      * @param cache object
-     * @param connectTimeout when connecting to remote resource
-     * @param readTimeout when connecting to remote resource
-     * @param headers HTTP headers to be sent together with request
+     * @param options for job (such as http headers, timeouts etc.)
      * @param downloadExecutor that will be executing the jobs
      */
     public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
             ICacheAccess<String, BufferedImageCacheEntry> cache,
-            int connectTimeout, int readTimeout, Map<String, String> headers,
+            TileJobOptions options,
             ThreadPoolExecutor downloadExecutor) {
-        super(cache, connectTimeout, readTimeout, headers, downloadExecutor);
+        super(cache, options, downloadExecutor);
         this.tile = tile;
+        this.options = options;
         if (listener != null) {
             String deduplicationKey = getCacheKey();
@@ -245,8 +247,8 @@
         // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles
         // at least for some short period of time, but not too long
-        if (ret.getExpirationTime() < now + MINIMUM_EXPIRES.get()) {
+        if (ret.getExpirationTime() < now + Math.max(MINIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) {
             ret.setExpirationTime(now + MINIMUM_EXPIRES.get());
         }
-        if (ret.getExpirationTime() > now + MAXIMUM_EXPIRES.get()) {
+        if (ret.getExpirationTime() > now + Math.max(MAXIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) {
             ret.setExpirationTime(now + MAXIMUM_EXPIRES.get());
         }
Index: trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java	(revision 13733)
@@ -59,4 +59,5 @@
         super(info, tileProjection);
         this.serverProjections = new TreeSet<>(info.getServerProjections());
+        this.headers.putAll(info.getCustomHttpHeaders());
         handleTemplate();
         initProjection();
@@ -109,12 +110,5 @@
             switchLatLon = Main.getProjection().switchXY();
         }
-        String bbox;
-        if (switchLatLon) {
-            bbox = String.format("%s,%s,%s,%s",
-                    LATLON_FORMAT.format(s), LATLON_FORMAT.format(w), LATLON_FORMAT.format(n), LATLON_FORMAT.format(e));
-        } else {
-            bbox = String.format("%s,%s,%s,%s",
-                    LATLON_FORMAT.format(w), LATLON_FORMAT.format(s), LATLON_FORMAT.format(e), LATLON_FORMAT.format(n));
-        }
+        String bbox = getBbox(zoom, tilex, tiley, switchLatLon);
 
         // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll
Index: trunk/src/org/openstreetmap/josm/data/imagery/TileJobOptions.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TileJobOptions.java	(revision 13733)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TileJobOptions.java	(revision 13733)
@@ -0,0 +1,66 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Class containing all options that are passed from Layer to TileJob
+ *
+ * @author Wiktor Niesiobedzki
+ *
+ */
+public class TileJobOptions {
+
+    final int connectTimeout;
+    final int readTimeout;
+    final Map<String, String> headers;
+    final long minimumExpiryTime;
+
+    /**
+     * Options constructor
+     *
+     * @param connectTimeout in milliseconds
+     * @param readTimeout in milliseconds
+     * @param headers
+     * @param minimumExpiryTime in seconds
+     */
+    public TileJobOptions(int connectTimeout, int readTimeout, Map<String, String> headers, long minimumExpiryTime) {
+        this.connectTimeout = connectTimeout;
+        this.readTimeout = readTimeout;
+        this.headers = Collections.unmodifiableMap(headers == null ? Collections.emptyMap() : headers);
+        this.minimumExpiryTime = minimumExpiryTime;
+    }
+
+    /**
+     *
+     * @return socket connection timeout in milliseconds
+     */
+    public int getConnectionTimeout() {
+        return connectTimeout;
+    }
+
+    /**
+     *
+     * @return socket read timeout in milliseconds
+     */
+    public int getReadTimeout() {
+        return readTimeout;
+    }
+
+    /**
+     *
+     * @return unmodifiable map with headers to be sent to tile server
+     */
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
+
+    /**
+     *
+     * @return minimum cache expire time in seconds for downloaded tiles
+     */
+    public long getMinimumExpiryTime() {
+        return minimumExpiryTime;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java	(revision 13733)
@@ -19,6 +19,7 @@
      * @param listener that will be notified, when tile has finished loading
      * @param headers that will be sent with requests to TileSource. <code>null</code> indicates none
+     * @param minimumExpiryTime minimum expiry time
      * @return TileLoader that uses both of above
      */
-    TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
+    TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers, long minimumExpiryTime);
 }
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java	(revision 13733)
@@ -1,6 +1,4 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.imagery;
-
-import java.util.Map;
 
 import org.apache.commons.jcs.access.behavior.ICacheAccess;
@@ -35,7 +33,7 @@
      */
     public WMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
-            int connectTimeout, int readTimeout, Map<String, String> headers) {
+            TileJobOptions options) {
 
-        super(listener, cache, connectTimeout, readTimeout, headers);
+        super(listener, cache, options);
         setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("WMS-downloader-%d", THREAD_LIMIT.get()));
     }
@@ -43,5 +41,5 @@
     @Override
     public TileJob createTileLoaderJob(Tile tile) {
-        return new WMSCachedTileLoaderJob(listener, tile, cache, connectTimeout, readTimeout, headers, getDownloadExecutor());
+        return new WMSCachedTileLoaderJob(listener, tile, cache, options, getDownloadExecutor());
     }
 }
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java	(revision 13733)
@@ -2,5 +2,4 @@
 package org.openstreetmap.josm.data.imagery;
 
-import java.util.Map;
 import java.util.concurrent.ThreadPoolExecutor;
 
@@ -23,13 +22,13 @@
      * @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 options options for tile job
      * @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);
+    public WMSCachedTileLoaderJob(TileLoaderListener listener,
+            Tile tile,
+            ICacheAccess<String, BufferedImageCacheEntry> cache,
+            TileJobOptions options,
+            ThreadPoolExecutor downloadExecutor) {
+        super(listener, tile, cache, options, downloadExecutor);
     }
 
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMSEndpointTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMSEndpointTileSource.java	(revision 13733)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMSEndpointTileSource.java	(revision 13733)
@@ -0,0 +1,101 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
+import org.openstreetmap.josm.io.imagery.WMSImagery;
+import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * Class representing ImageryType.WMS_ENDPOINT tile source.
+ * It differs from standard WMS tile source that this tile source fetches GetCapabilities from server and
+ * uses most of the parameters from there
+ *
+ * @author Wiktor Niesiobedzki
+ *
+ */
+public class WMSEndpointTileSource extends AbstractWMSTileSource implements TemplatedTileSource {
+
+    private final WMSImagery wmsi;
+    private List<DefaultLayer> layers;
+    private String urlPattern;
+    private static final Pattern PATTERN_PARAM  = Pattern.compile("\\{([^}]+)\\}");
+    private final Map<String, String> headers = new ConcurrentHashMap<>();
+
+    /**
+     * Create WMSEndpointTileSource tile source
+     * @param info WMS_ENDPOINT ImageryInfo
+     * @param tileProjection server projection that should be used by this tile source
+     */
+    public WMSEndpointTileSource(ImageryInfo info, Projection tileProjection) {
+        super(info, tileProjection);
+        CheckParameterUtil.ensure(info, "imageryType", x -> ImageryType.WMS_ENDPOINT.equals(x.getImageryType()));
+        try {
+            wmsi = new WMSImagery(info.getUrl());
+        } catch (IOException | WMSGetCapabilitiesException e) {
+            throw new IllegalArgumentException(e);
+        }
+        layers = info.getDefaultLayers();
+        initProjection();
+        urlPattern = wmsi.buildGetMapUrl(layers, info.isTransparent());
+        this.headers.putAll(info.getCustomHttpHeaders());
+    }
+
+    @Override
+    public int getDefaultTileSize() {
+        return WMSLayer.PROP_IMAGE_SIZE.get();
+    }
+
+    @Override
+    public String getTileUrl(int zoom, int tilex, int tiley) {
+        String bbox = getBbox(zoom, tilex, tiley, wmsi.belowWMS130() ? false : getTileProjection().switchXY());
+
+        // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll
+        StringBuffer url = new StringBuffer(urlPattern.length());
+        Matcher matcher = PATTERN_PARAM.matcher(urlPattern);
+        while (matcher.find()) {
+            String replacement;
+            switch (matcher.group(1)) {
+            case "proj":
+                replacement = getServerCRS();
+                break;
+            case "bbox":
+                replacement = bbox;
+                break;
+            case "width":
+            case "height":
+                replacement = String.valueOf(getTileSize());
+                break;
+            default:
+                replacement = '{' + matcher.group(1) + '}';
+            }
+            matcher.appendReplacement(url, replacement);
+        }
+        matcher.appendTail(url);
+        return url.toString();
+    }
+
+    /**
+     *
+     * @return list of EPSG codes that current layer selection supports (this may differ from layer to layer)
+     */
+    public List<String> getServerProjections() {
+        return wmsi.getLayers(layers).stream().flatMap(x -> x.getCrs().stream()).distinct().collect(Collectors.toList());
+    }
+
+    @Override
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMTSCapabilities.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMTSCapabilities.java	(revision 13733)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMTSCapabilities.java	(revision 13733)
@@ -0,0 +1,63 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import java.util.Collection;
+
+import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.TransferMode;
+import org.openstreetmap.josm.data.imagery.WMTSTileSource.Layer;
+
+/**
+ * Data object containing WMTS GetCapabilities document
+ *
+ * @author Wiktor Niesiobedzki
+ *
+ */
+public class WMTSCapabilities {
+    private String baseUrl;
+    private TransferMode transferMode;
+    private Collection<Layer> layers;
+
+
+    /**
+     *
+     * @param baseUrl of this service
+     * @param transferMode either KVP (key-value pairs in URL parameters) or RESTful (part of path)
+     */
+    public WMTSCapabilities(String baseUrl, TransferMode transferMode) {
+        this.baseUrl = baseUrl;
+        this.transferMode = transferMode;
+    }
+
+    /**
+     *
+     * @param layers layers to add to this document
+     */
+    public void addLayers(Collection<Layer> layers) {
+        this.layers = layers;
+
+    }
+
+    /**
+     *
+     * @return layers defined by this service
+     */
+    public Collection<Layer> getLayers() {
+        return layers;
+    }
+
+    /**
+     *
+     * @return base url for this service
+     */
+    public String getBaseUrl() {
+        return baseUrl;
+    }
+
+    /**
+     *
+     * @return transfer mode (KVP or RESTful) for this service
+     */
+    public TransferMode getTransferMode() {
+        return transferMode;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMTSDefaultLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMTSDefaultLayer.java	(revision 13732)
+++ 	(revision )
@@ -1,28 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.data.imagery;
-
-/**
- * WMTS default layer.
- * @since 11257
- */
-public class WMTSDefaultLayer extends DefaultLayer {
-    private final String tileMatrixSet;
-
-    /**
-     * Constructs a new {@code WMTSDefaultLayer}.
-     * @param layerName layer name
-     * @param tileMatrixSet tile matrix set
-     */
-    public WMTSDefaultLayer(String layerName, String tileMatrixSet) {
-        super(layerName);
-        this.tileMatrixSet = tileMatrixSet;
-    }
-
-    /**
-     * Returns the tile matrix set.
-     * @return the tile matrix set
-     */
-    public String getTileMatrixSet() {
-        return tileMatrixSet;
-    }
-}
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 13733)
@@ -51,4 +51,6 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.TransferMode;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.data.projection.Projections;
@@ -126,5 +128,11 @@
     }
 
-    private static class TileMatrixSet {
+    /**
+     *
+     * class representing WMTS TileMatrixSet
+     * This connects projection and TileMatrix (how the map is divided in tiles)
+     *
+     */
+    public static class TileMatrixSet {
 
         private final List<TileMatrix> tileMatrix;
@@ -154,4 +162,12 @@
             return "TileMatrixSet [crs=" + crs + ", identifier=" + identifier + ']';
         }
+
+        /**
+         *
+         * @return identifier of this TileMatrixSet
+         */
+        public String getIdentifier() {
+            return identifier;
+        }
     }
 
@@ -162,5 +178,9 @@
     }
 
-    private static class Layer {
+    /**
+     * Class representing WMTS Layer information
+     *
+     */
+    public static class Layer {
         private String format;
         private String identifier;
@@ -202,4 +222,42 @@
                     + tileMatrixSet + ", baseUrl=" + baseUrl + ", style=" + style + ']';
         }
+
+        /**
+         *
+         * @return identifier of this layer
+         */
+        public String getIdentifier() {
+            return identifier;
+        }
+
+        /**
+         *
+         * @return style of this layer
+         */
+        public String getStyle() {
+            return style;
+        }
+
+        /**
+         *
+         * @return
+         */
+        public TileMatrixSet getTileMatrixSet() {
+            return tileMatrixSet;
+        }
+    }
+
+    /**
+     * Exception thrown when praser doesn't find expected information in GetCapabilities document
+     *
+     */
+    public static class WMTSGetCapabilitiesException extends Exception {
+
+        /**
+         * @param cause description of cause
+         */
+        public WMTSGetCapabilitiesException(String cause) {
+            super(cause);
+        }
     }
 
@@ -211,55 +269,5 @@
             super(Main.parent, tr("Select WMTS layer"), tr("Add layers"), tr("Cancel"));
             this.layers = groupLayersByNameAndTileMatrixSet(layers);
-            //getLayersTable(layers, Main.getProjection())
-            this.list = new JTable(
-                    new AbstractTableModel() {
-                        @Override
-                        public Object getValueAt(int rowIndex, int columnIndex) {
-                            switch (columnIndex) {
-                            case 0:
-                                return SelectLayerDialog.this.layers.get(rowIndex).getValue()
-                                        .stream()
-                                        .map(Layer::getUserTitle)
-                                        .collect(Collectors.joining(", ")); //this should be only one
-                            case 1:
-                                return SelectLayerDialog.this.layers.get(rowIndex).getValue()
-                                        .stream()
-                                        .map(x -> x.tileMatrixSet.crs)
-                                        .collect(Collectors.joining(", "));
-                            case 2:
-                                return SelectLayerDialog.this.layers.get(rowIndex).getValue()
-                                        .stream()
-                                        .map(x -> x.tileMatrixSet.identifier)
-                                        .collect(Collectors.joining(", ")); //this should be only one
-                            default:
-                                throw new IllegalArgumentException();
-                            }
-                        }
-
-                        @Override
-                        public int getRowCount() {
-                            return SelectLayerDialog.this.layers.size();
-                        }
-
-                        @Override
-                        public int getColumnCount() {
-                            return 3;
-                        }
-
-                        @Override
-                        public String getColumnName(int column) {
-                            switch (column) {
-                            case 0: return tr("Layer name");
-                            case 1: return tr("Projection");
-                            case 2: return tr("Matrix set identifier");
-                            default:
-                                throw new IllegalArgumentException();
-                            }
-                        }
-                    });
-            this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-            this.list.setAutoCreateRowSorter(true);
-            this.list.setRowSelectionAllowed(true);
-            this.list.setColumnSelectionAllowed(false);
+            this.list = getLayerSelectionPanel(this.layers);
             JPanel panel = new JPanel(new GridBagLayout());
             panel.add(new JScrollPane(this.list), GBC.eol().fill());
@@ -273,12 +281,7 @@
             }
             Layer selectedLayer = layers.get(list.convertRowIndexToModel(index)).getValue().get(0);
-            return new WMTSDefaultLayer(selectedLayer.identifier, selectedLayer.tileMatrixSet.identifier);
-        }
-
-        private static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
-            Map<String, List<Layer>> layerByName = layers.stream().collect(
-                    Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
-            return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
-        }
+            return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
+        }
+
     }
 
@@ -292,5 +295,5 @@
     private ScaleList nativeScaleList;
 
-    private final WMTSDefaultLayer defaultLayer;
+    private final DefaultLayer defaultLayer;
 
     private Projection tileProjection;
@@ -300,27 +303,26 @@
      * @param info imagery info
      * @throws IOException if any I/O error occurs
+     * @throws WMTSGetCapabilitiesException
      * @throws IllegalArgumentException if any other error happens for the given imagery info
      */
-    public WMTSTileSource(ImageryInfo info) throws IOException {
+    public WMTSTileSource(ImageryInfo info) throws IOException, WMTSGetCapabilitiesException {
         super(info);
         CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
-
+        this.headers.putAll(info.getCustomHttpHeaders());
         this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
-        this.layers = getCapabilities();
+        WMTSCapabilities capabilities = getCapabilities(baseUrl, headers);
+        this.layers =  capabilities.getLayers();
+        this.baseUrl = capabilities.getBaseUrl();
+        this.transferMode = capabilities.getTransferMode();
         if (info.getDefaultLayers().isEmpty()) {
             Logging.warn(tr("No default layer selected, choosing first layer."));
             if (!layers.isEmpty()) {
                 Layer first = layers.iterator().next();
-                this.defaultLayer = new WMTSDefaultLayer(first.identifier, first.tileMatrixSet.identifier);
+                this.defaultLayer = new DefaultLayer(info.getImageryType(), first.identifier, first.style, first.tileMatrixSet.identifier);
             } else {
                 this.defaultLayer = null;
             }
         } else {
-            DefaultLayer defLayer = info.getDefaultLayers().iterator().next();
-            if (defLayer instanceof WMTSDefaultLayer) {
-                this.defaultLayer = (WMTSDefaultLayer) defLayer;
-            } else {
-                this.defaultLayer = null;
-            }
+            this.defaultLayer = info.getDefaultLayers().iterator().next();
         }
         if (this.layers.isEmpty())
@@ -343,5 +345,5 @@
                 // only one tile matrix set with matching projection - no point in asking
                 Layer selectedLayer = ls.get(0);
-                return new WMTSDefaultLayer(selectedLayer.identifier, selectedLayer.tileMatrixSet.identifier);
+                return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
             }
         }
@@ -366,11 +368,15 @@
     }
 
-    /**
+
+    /**
+     * @param url of the getCapabilities document
+     * @param headers HTTP headers to set when calling getCapabilities url
      * @return capabilities
      * @throws IOException in case of any I/O error
+     * @throws WMTSGetCapabilitiesException
      * @throws IllegalArgumentException in case of any other error
      */
-    private Collection<Layer> getCapabilities() throws IOException {
-        try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers).
+    public static WMTSCapabilities getCapabilities(String url, Map<String, String> headers) throws IOException, WMTSGetCapabilitiesException {
+        try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
                 setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)).
                 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
@@ -379,21 +385,37 @@
             if (data.length == 0) {
                 cf.clear();
-                throw new IllegalArgumentException("Could not read data from: " + baseUrl);
+                throw new IllegalArgumentException("Could not read data from: " + url);
             }
 
             try {
                 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data));
-                Collection<Layer> ret = new ArrayList<>();
+                WMTSCapabilities ret = null;
+                Collection<Layer> layers = null;
                 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
                     if (event == XMLStreamReader.START_ELEMENT) {
                         if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) {
-                            parseOperationMetadata(reader);
+                            ret = parseOperationMetadata(reader);
                         }
 
                         if (QN_CONTENTS.equals(reader.getName())) {
-                            ret = parseContents(reader);
+                            layers = parseContents(reader);
                         }
                     }
                 }
+                if (ret == null) {
+                    /*
+                     *  see #12168 - create dummy operation metadata - not all WMTS services provide this information
+                     *
+                     *  WMTS Standard:
+                     *  > Resource oriented architecture style HTTP encodings SHALL not be described in the OperationsMetadata section.
+                     *
+                     *  And OperationMetada is not mandatory element. So REST mode is justifiable
+                     */
+                    ret = new WMTSCapabilities(url, TransferMode.REST);
+                }
+                if (layers == null) {
+                    throw new WMTSGetCapabilitiesException(tr("WMTS Capabilties document did not contain layers in url:  {0}", url));
+                }
+                ret.addLayers(layers);
                 return ret;
             } catch (XMLStreamException e) {
@@ -456,4 +478,7 @@
         supportedMimeTypes.add("image/jpgpng");         // used by ESRI
         supportedMimeTypes.add("image/png8");           // used by geoserver
+        if (supportedMimeTypes.contains("image/jpeg")) {
+            supportedMimeTypes.add("image/jpg"); // sometimes mispelled by Arcgis
+        }
         Collection<String> unsupportedFormats = new ArrayList<>();
 
@@ -639,10 +664,11 @@
     /**
      * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
-     * Sets this.baseUrl and this.transferMode
+     * return WMTSCapabilities with baseUrl and transferMode
      *
      * @param reader StAX reader instance
+     * @return WMTSCapabilities with baseUrl and transferMode set
      * @throws XMLStreamException See {@link XMLStreamReader}
      */
-    private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
+    private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
         for (int event = reader.getEventType();
                 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
@@ -657,8 +683,11 @@
                             GetCapabilitiesParseHelper.QN_OWS_GET
                     )) {
-                this.baseUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
-                this.transferMode = GetCapabilitiesParseHelper.getTransferMode(reader);
-            }
-        }
+                return new WMTSCapabilities(
+                        reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"),
+                        GetCapabilitiesParseHelper.getTransferMode(reader)
+                        );
+            }
+        }
+        return null;
     }
 
@@ -671,5 +700,5 @@
             return;
         List<Layer> matchingLayers = layers.stream().filter(
-                l -> l.identifier.equals(defaultLayer.layerName) && l.tileMatrixSet.crs.equals(proj.toCode()))
+                l -> l.identifier.equals(defaultLayer.getLayerName()) && l.tileMatrixSet.crs.equals(proj.toCode()))
                 .collect(Collectors.toList());
         if (matchingLayers.size() > 1) {
@@ -686,5 +715,5 @@
                 this.tileProjection = null;
                 for (Layer layer : layers) {
-                    if (!layer.identifier.equals(defaultLayer.layerName)) {
+                    if (!layer.identifier.equals(defaultLayer.getLayerName())) {
                         continue;
                     }
@@ -922,4 +951,65 @@
     }
 
+    public static JTable getLayerSelectionPanel(List<Entry<String, List<Layer>>> layers) {
+        JTable list = new JTable(
+                new AbstractTableModel() {
+                    @Override
+                    public Object getValueAt(int rowIndex, int columnIndex) {
+                        switch (columnIndex) {
+                        case 0:
+                            return layers.get(rowIndex).getValue()
+                                    .stream()
+                                    .map(Layer::getUserTitle)
+                                    .collect(Collectors.joining(", ")); //this should be only one
+                        case 1:
+                            return layers.get(rowIndex).getValue()
+                                    .stream()
+                                    .map(x -> x.tileMatrixSet.crs)
+                                    .collect(Collectors.joining(", "));
+                        case 2:
+                            return layers.get(rowIndex).getValue()
+                                    .stream()
+                                    .map(x -> x.tileMatrixSet.identifier)
+                                    .collect(Collectors.joining(", ")); //this should be only one
+                        default:
+                            throw new IllegalArgumentException();
+                        }
+                    }
+
+                    @Override
+                    public int getRowCount() {
+                        return layers.size();
+                    }
+
+                    @Override
+                    public int getColumnCount() {
+                        return 3;
+                    }
+
+                    @Override
+                    public String getColumnName(int column) {
+                        switch (column) {
+                        case 0: return tr("Layer name");
+                        case 1: return tr("Projection");
+                        case 2: return tr("Matrix set identifier");
+                        default:
+                            throw new IllegalArgumentException();
+                        }
+                    }
+                });
+        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        list.setAutoCreateRowSorter(true);
+        list.setRowSelectionAllowed(true);
+        list.setColumnSelectionAllowed(false);
+        return list;
+    }
+
+    public static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
+        Map<String, List<Layer>> layerByName = layers.stream().collect(
+                Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
+        return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
+    }
+
+
     /**
      * @return set of projection codes that this TileSource supports
Index: trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java	(revision 13733)
@@ -21,4 +21,5 @@
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
 
 import javax.swing.ButtonModel;
@@ -158,5 +159,5 @@
         TileLoaderFactory cachedLoaderFactory = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class);
         if (cachedLoaderFactory != null) {
-            cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers);
+            cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers, TimeUnit.HOURS.toSeconds(1));
         } else {
             cachedLoader = null;
Index: trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java	(revision 13733)
@@ -203,4 +203,5 @@
     // prepared to be moved to the painter
     protected TileCoordinateConverter coordinateConverter;
+    private final long minimumTileExpire;
 
     /**
@@ -214,4 +215,5 @@
         getFilterSettings().addFilterChangeListener(this);
         getDisplaySettings().addSettingsChangeListener(this);
+        this.minimumTileExpire = info.getMinimumTileExpire();
     }
 
@@ -274,5 +276,5 @@
         Map<String, String> headers = getHeaders(tileSource);
 
-        tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
+        tileLoader = getTileLoaderFactory().makeTileLoader(this, headers, minimumTileExpire);
 
         try {
@@ -1759,5 +1761,5 @@
         public PrecacheTask(ProgressMonitor progressMonitor) {
             this.progressMonitor = progressMonitor;
-            this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
+            this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource), minimumTileExpire);
             if (this.tileLoader instanceof TMSCachedTileLoader) {
                 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
Index: trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java	(revision 13733)
@@ -196,4 +196,5 @@
         switch(info.getImageryType()) {
         case WMS:
+        case WMS_ENDPOINT:
             return new WMSLayer(info);
         case WMTS:
Index: trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 13733)
@@ -25,4 +25,5 @@
 import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
 import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
+import org.openstreetmap.josm.data.imagery.WMSEndpointTileSource;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
@@ -57,5 +58,5 @@
     private static final String CACHE_REGION_NAME = "WMS";
 
-    private final List<String> serverProjections;
+    private List<String> serverProjections;
 
     /**
@@ -65,7 +66,10 @@
     public WMSLayer(ImageryInfo info) {
         super(info);
-        CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS, "ImageryType is WMS");
+        CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS || info.getImageryType() == ImageryType.WMS_ENDPOINT, "ImageryType is WMS");
         CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url");
-        TemplatedWMSTileSource.checkUrl(info.getUrl());
+        if (info.getImageryType() == ImageryType.WMS) {
+            TemplatedWMSTileSource.checkUrl(info.getUrl());
+
+        }
         this.serverProjections = new ArrayList<>(info.getServerProjections());
     }
@@ -89,6 +93,22 @@
     @Override
     protected AbstractWMSTileSource getTileSource() {
-        AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(
-                info, chooseProjection(Main.getProjection()));
+        AbstractWMSTileSource tileSource;
+        if (info.getImageryType() == ImageryType.WMS) {
+            tileSource = new TemplatedWMSTileSource(info, chooseProjection(Main.getProjection()));
+        } else {
+            /*
+             *  Chicken-and-egg problem. We want to create tile source, but supported projections we can get only
+             *  from this tile source. So create tilesource first with dummy Main.getProjection(), and then update
+             *  once we update server projections.
+             *
+             *  Thus:
+             *  * it is not required to provide projections for wms_endpoint imagery types
+             *  * we always use current definitions returned by server
+             */
+            WMSEndpointTileSource endpointTileSource = new WMSEndpointTileSource(info, Main.getProjection());
+            this.serverProjections = endpointTileSource.getServerProjections();
+            endpointTileSource.setTileProjection(chooseProjection(Main.getProjection()));
+            tileSource = endpointTileSource;
+        }
         info.setAttribution(tileSource);
         return tileSource;
Index: trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java	(revision 13733)
@@ -13,4 +13,5 @@
 import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
 import org.openstreetmap.josm.data.imagery.WMTSTileSource;
+import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -64,5 +65,5 @@
             }
             return null;
-        } catch (IOException e) {
+        } catch (IOException | WMTSGetCapabilitiesException e) {
             Logging.warn(e);
             throw new IllegalArgumentException(e);
Index: trunk/src/org/openstreetmap/josm/gui/preferences/imagery/HeadersTable.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/imagery/HeadersTable.java	(revision 13733)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/imagery/HeadersTable.java	(revision 13733)
@@ -0,0 +1,120 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+import javax.swing.table.AbstractTableModel;
+
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Simple table for editing HTTP headers
+ * @author Wiktor Niesiobedzki
+ *
+ */
+public class HeadersTable extends JPanel {
+
+    private final class HeaderTableModel extends AbstractTableModel {
+        @Override
+        public String getColumnName(int column) {
+            switch (column) {
+            case 0:
+                return tr("Header name");
+            case 1:
+                return tr("Header value");
+            default:
+                return "";
+            }
+        }
+
+        @Override
+        public int getRowCount() {
+            return headers.size() + 1;
+        }
+
+        @Override
+        public int getColumnCount() {
+            return 2;
+        }
+
+        @Override
+        public Object getValueAt(int row, int col) {
+            if (row < headers.size()) {
+                return headers.get(row)[col];
+            }
+            return "";
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return true;
+        }
+
+        @Override
+        public void setValueAt(Object value, int row, int col) {
+            if (row < headers.size()) {
+                String[] headerRow = headers.get(row);
+                headerRow[col] = (String) value;
+                if ("".equals(headerRow[0]) && "".equals(headerRow[1])) {
+                    headers.remove(row);
+                    fireTableRowsDeleted(row, row);
+                }
+
+            } else if (row == headers.size()) {
+                String[] entry = new String[] { "", "" };
+                entry[col] = (String) value;
+                headers.add(entry);
+                fireTableRowsInserted(row + 1, row + 1);
+            }
+            fireTableCellUpdated(row, col);
+        }
+    }
+
+    private final JTable table;
+    private List<String[]> headers;
+
+    /**
+     * Creates empty table
+     */
+    public HeadersTable() {
+        this(new ConcurrentHashMap<>());
+    }
+
+    /**
+     * Create table prefilled with headers
+     * @param headers
+     */
+    public HeadersTable(Map<String, String> headers) {
+        super(new GridBagLayout());
+        this.headers = getHeadersAsVector(headers);
+        table = new JTable(new HeaderTableModel());
+        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        table.setAutoCreateRowSorter(true);
+        table.setRowSelectionAllowed(false);
+        table.setColumnSelectionAllowed(false);
+        add(new JScrollPane(table), GBC.eol().fill());
+    }
+
+    private static List<String[]> getHeadersAsVector(Map<String, String> headers) {
+        return headers.entrySet().stream().sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey()))
+                .map(e -> new String[] { e.getKey(), e.getValue() }).collect(Collectors.toList());
+    }
+
+    /**
+     * @return headers provided by user
+     */
+    public Map<String, String> getHeaders() {
+        return headers.stream().distinct().collect(Collectors.toMap(x -> x[0], x -> x[1]));
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java	(revision 13733)
@@ -7,12 +7,13 @@
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Stack;
+import java.util.concurrent.ConcurrentHashMap;
 
 import javax.xml.parsers.ParserConfigurationException;
 
+import org.openstreetmap.josm.data.imagery.DefaultLayer;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
@@ -57,5 +58,8 @@
         NO_TILESUM,
         METADATA,
-        UNKNOWN,            // element is not recognized in the current context
+        DEFAULT_LAYERS,
+        CUSTOM_HTTP_HEADERS,
+        NOOP,
+        UNKNOWN,             // element is not recognized in the current context
     }
 
@@ -132,4 +136,6 @@
         private MultiMap<String, String> noTileChecksums;
         private Map<String, String> metadataHeaders;
+        private List<DefaultLayer> defaultLayers;
+        private Map<String, String> customHttpHeaders;
 
         @Override
@@ -145,4 +151,5 @@
             noTileHeaders = null;
             noTileChecksums = null;
+            customHttpHeaders = null;
         }
 
@@ -164,5 +171,7 @@
                     noTileHeaders = new MultiMap<>();
                     noTileChecksums = new MultiMap<>();
-                    metadataHeaders = new HashMap<>();
+                    metadataHeaders = new ConcurrentHashMap<>();
+                    defaultLayers = new ArrayList<>();
+                    customHttpHeaders = new ConcurrentHashMap<>();
                     String best = atts.getValue("eli-best");
                     if (TRUE.equals(best)) {
@@ -215,5 +224,7 @@
                         TILE_SIZE,
                         "valid-georeference",
-                        "mod-tile-features"
+                        "mod-tile-features",
+                        "transparent",
+                        "minimum-tile-expire"
                 ).contains(qName)) {
                     newState = State.ENTRY_ATTRIBUTE;
@@ -247,4 +258,9 @@
                     metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key"));
                     newState = State.METADATA;
+                } else if ("defaultLayers".equals(qName)) {
+                    newState = State.DEFAULT_LAYERS;
+                } else if ("custom-http-header".equals(qName)) {
+                   customHttpHeaders.put(atts.getValue("header-name"), atts.getValue("header-value"));
+                   newState = State.CUSTOM_HTTP_HEADERS;
                 }
                 break;
@@ -269,4 +285,10 @@
                 if ("code".equals(qName)) {
                     newState = State.CODE;
+                }
+                break;
+            case DEFAULT_LAYERS:
+                if ("layer".equals(qName)) {
+                    newState = State.NOOP;
+                    defaultLayers.add(new DefaultLayer(entry.getImageryType(), atts.getValue("name"),atts.getValue("style"), atts.getValue("tileMatrixSet")));
                 }
                 break;
@@ -306,4 +328,8 @@
                     entry.setMetadataHeaders(metadataHeaders);
                     metadataHeaders = null;
+                    entry.setDefaultLayers(defaultLayers);
+                    defaultLayers = null;
+                    entry.setCustomHttpHeaders(customHttpHeaders);
+                    customHttpHeaders = null;
 
                     if (!skipEntry) {
@@ -323,4 +349,5 @@
                     switch(qName) {
                     case "type":
+                        ImageryType.values();
                         boolean found = false;
                         for (ImageryType type : ImageryType.values()) {
@@ -487,4 +514,10 @@
                 case "mod-tile-features":
                     entry.setModTileFeatures(Boolean.parseBoolean(accumulator.toString()));
+                    break;
+                case "transparent":
+                    entry.setTransparent(Boolean.parseBoolean(accumulator.toString()));
+                    break;
+                case "minimum-tile-expire":
+                    entry.setMinimumTileExpire(Integer.valueOf(accumulator.toString()));
                     break;
                 default: // Do nothing
Index: trunk/src/org/openstreetmap/josm/io/imagery/WMSImagery.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/WMSImagery.java	(revision 13732)
+++ trunk/src/org/openstreetmap/josm/io/imagery/WMSImagery.java	(revision 13733)
@@ -2,9 +2,10 @@
 package org.openstreetmap.josm.io.imagery;
 
-import java.awt.HeadlessException;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.StringReader;
-import java.io.StringWriter;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -13,38 +14,28 @@
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
-import java.util.NoSuchElementException;
+import java.util.Map;
 import java.util.Set;
-import java.util.regex.Matcher;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
 
 import javax.imageio.ImageIO;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.TransformerFactoryConfigurationError;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
 
 import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.imagery.DefaultLayer;
 import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.LayerDetails;
+import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.data.projection.Projections;
-import org.openstreetmap.josm.tools.HttpClient;
-import org.openstreetmap.josm.tools.HttpClient.Response;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
 
 /**
@@ -53,34 +44,43 @@
 public class WMSImagery {
 
-    private static final class ChildIterator implements Iterator<Element> {
-        private Element child;
-
-        ChildIterator(Element parent) {
-            child = advanceToElement(parent.getFirstChild());
-        }
-
-        private static Element advanceToElement(Node firstChild) {
-            Node node = firstChild;
-            while (node != null && !(node instanceof Element)) {
-                node = node.getNextSibling();
-            }
-            return (Element) node;
-        }
-
-        @Override
-        public boolean hasNext() {
-            return child != null;
-        }
-
-        @Override
-        public Element next() {
-            if (!hasNext()) {
-                throw new NoSuchElementException("No next sibling.");
-            }
-            Element next = child;
-            child = advanceToElement(child.getNextSibling());
-            return next;
-        }
-    }
+
+    private static final String CAPABILITIES_QUERY_STRING = "SERVICE=WMS&REQUEST=GetCapabilities";
+
+    /**
+     * WMS namespace address
+     */
+    public static final String WMS_NS_URL = "http://www.opengis.net/wms";
+
+    // CHECKSTYLE.OFF: SingleSpaceSeparator
+    // WMS 1.0 - 1.3.0
+    private static final QName CAPABILITITES_ROOT_130 = new QName("WMS_Capabilities", WMS_NS_URL);
+    private static final QName QN_ABSTRACT            = new QName(WMS_NS_URL, "Abstract");
+    private static final QName QN_CAPABILITY          = new QName(WMS_NS_URL, "Capability");
+    private static final QName QN_CRS                 = new QName(WMS_NS_URL, "CRS");
+    private static final QName QN_DCPTYPE             = new QName(WMS_NS_URL, "DCPType");
+    private static final QName QN_FORMAT              = new QName(WMS_NS_URL, "Format");
+    private static final QName QN_GET                 = new QName(WMS_NS_URL, "Get");
+    private static final QName QN_GETMAP              = new QName(WMS_NS_URL, "GetMap");
+    private static final QName QN_HTTP                = new QName(WMS_NS_URL, "HTTP");
+    private static final QName QN_LAYER               = new QName(WMS_NS_URL, "Layer");
+    private static final QName QN_NAME                = new QName(WMS_NS_URL, "Name");
+    private static final QName QN_REQUEST             = new QName(WMS_NS_URL, "Request");
+    private static final QName QN_SERVICE             = new QName(WMS_NS_URL, "Service");
+    private static final QName QN_STYLE               = new QName(WMS_NS_URL, "Style");
+    private static final QName QN_TITLE               = new QName(WMS_NS_URL, "Title");
+    private static final QName QN_BOUNDINGBOX         = new QName(WMS_NS_URL, "BoundingBox");
+    private static final QName QN_EX_GEOGRAPHIC_BBOX  = new QName(WMS_NS_URL, "EX_GeographicBoundingBox");
+    private static final QName QN_WESTBOUNDLONGITUDE  = new QName(WMS_NS_URL, "westBoundLongitude");
+    private static final QName QN_EASTBOUNDLONGITUDE  = new QName(WMS_NS_URL, "eastBoundLongitude");
+    private static final QName QN_SOUTHBOUNDLATITUDE  = new QName(WMS_NS_URL, "southBoundLatitude");
+    private static final QName QN_NORTHBOUNDLATITUDE  = new QName(WMS_NS_URL, "northBoundLatitude");
+    private static final QName QN_ONLINE_RESOURCE     = new QName(WMS_NS_URL, "OnlineResource");
+
+    // WMS 1.1 - 1.1.1
+    private static final QName CAPABILITIES_ROOT_111 = new QName("WMT_MS_Capabilities");
+    private static final QName QN_SRS                = new QName("SRS");
+    private static final QName QN_LATLONBOUNDINGBOX  = new QName("LatLonBoundingBox");
+
+    // CHECKSTYLE.ON: SingleSpaceSeparator
 
     /**
@@ -120,12 +120,94 @@
     }
 
-    private List<LayerDetails> layers;
-    private URL serviceUrl;
-    private List<String> formats;
-    private String version = "1.1.1";
-
-    /**
-     * Returns the list of layers.
-     * @return the list of layers
+    private Map<String, String> headers = new ConcurrentHashMap<>();
+    private String version = "1.1.1"; // default version
+    private String getMapUrl;
+    private URL capabilitiesUrl;
+    private List<String> formats = new ArrayList<>();
+    private List<LayerDetails> layers = new ArrayList<>();
+
+    private String title;
+
+    /**
+     * Make getCapabilities request towards given URL
+     * @param url service url
+     * @throws IOException
+     * @throws WMSGetCapabilitiesException
+     */
+    public WMSImagery(String url) throws IOException, WMSGetCapabilitiesException {
+        this(url, null);
+    }
+
+    /**
+     * Make getCapabilities request towards given URL using headers
+     * @param url service url
+     * @param headers HTTP headers to be sent with request
+     * @throws IOException
+     * @throws WMSGetCapabilitiesException
+     */
+    public WMSImagery(String url, Map<String, String> headers) throws IOException, WMSGetCapabilitiesException {
+        if (headers != null) {
+            this.headers.putAll(headers);
+        }
+
+        IOException savedExc = null;
+        String workingAddress = null;
+        url_search:
+        for (String z: new String[]{
+                normalizeUrl(url),
+                url,
+                url + CAPABILITIES_QUERY_STRING,
+        }) {
+            for (String ver: new String[]{"", "&VERSION=1.3.0", "&VERSION=1.1.1"}) {
+                try {
+                    attemptGetCapabilities(z + ver);
+                    workingAddress = z;
+                    calculateChildren();
+                    // clear saved exception - we've got something working
+                    savedExc = null;
+                    break url_search;
+                } catch (IOException e) {
+                    savedExc = e;
+                    Logging.warn(e);
+                }
+            }
+        }
+
+        if (workingAddress != null) {
+            try {
+                capabilitiesUrl = new URL(workingAddress);
+            } catch (MalformedURLException e) {
+                if (savedExc != null) {
+                    savedExc = e;
+                }
+                try {
+                    capabilitiesUrl = new File(workingAddress).toURI().toURL();
+                } catch (MalformedURLException e1) {
+                    // do nothing, raise original exception
+                }
+            }
+        }
+
+        if (savedExc != null) {
+            throw savedExc;
+        }
+    }
+
+    private void calculateChildren() {
+        Map<LayerDetails, List<LayerDetails>> layerChildren = layers.stream()
+                .filter(x -> x.getParent() != null) // exclude top-level elements
+                .collect(Collectors.groupingBy(LayerDetails::getParent));
+        for (LayerDetails ld: layers) {
+            if (layerChildren.containsKey(ld)) {
+                ld.setChildren(layerChildren.get(ld));
+            }
+        }
+        // leave only top-most elements in the list
+        layers = layers.stream().filter(x -> x.getParent() == null).collect(Collectors.toCollection(ArrayList::new));
+    }
+
+    /**
+     * Returns the list of top-level layers.
+     * @return the list of top-level layers
      */
     public List<LayerDetails> getLayers() {
@@ -134,37 +216,20 @@
 
     /**
-     * Returns the service URL.
-     * @return the service URL
-     */
-    public URL getServiceUrl() {
-        return serviceUrl;
-    }
-
-    /**
-     * Returns the WMS version used.
-     * @return the WMS version used (1.1.1 or 1.3.0)
-     * @since 13358
-     */
-    public String getVersion() {
-        return version;
-    }
-
-    /**
      * Returns the list of supported formats.
      * @return the list of supported formats
      */
-    public List<String> getFormats() {
+    public Collection<String> getFormats() {
         return Collections.unmodifiableList(formats);
     }
 
     /**
-     * Gets the preffered format for this imagery layer.
-     * @return The preffered format as mime type.
-     */
-    public String getPreferredFormats() {
-        if (formats.contains("image/jpeg")) {
+     * Gets the preferred format for this imagery layer.
+     * @return The preferred format as mime type.
+     */
+    public String getPreferredFormat() {
+        if (formats.contains("image/png")) {
+            return "image/png";
+        } else if (formats.contains("image/jpeg")) {
             return "image/jpeg";
-        } else if (formats.contains("image/png")) {
-            return "image/png";
         } else if (formats.isEmpty()) {
             return null;
@@ -174,8 +239,16 @@
     }
 
-    String buildRootUrl() {
-        if (serviceUrl == null) {
+    /**
+     * @return root URL of services in this GetCapabilities
+     */
+    public String buildRootUrl() {
+        if (getMapUrl == null && capabilitiesUrl == null) {
             return null;
         }
+        if (getMapUrl != null) {
+            return getMapUrl;
+        }
+
+        URL serviceUrl = capabilitiesUrl;
         StringBuilder a = new StringBuilder(serviceUrl.getProtocol());
         a.append("://").append(serviceUrl.getHost());
@@ -194,174 +267,350 @@
 
     /**
-     * Returns the URL for the "GetMap" WMS request in JPEG format.
-     * @param selectedLayers the list of selected layers, matching the "LAYERS" WMS request argument
-     * @return the URL for the "GetMap" WMS request
-     */
-    public String buildGetMapUrl(Collection<LayerDetails> selectedLayers) {
-        return buildGetMapUrl(selectedLayers, "image/jpeg");
-    }
-
-    /**
-     * Returns the URL for the "GetMap" WMS request.
-     * @param selectedLayers the list of selected layers, matching the "LAYERS" WMS request argument
-     * @param format the requested image format, matching the "FORMAT" WMS request argument
-     * @return the URL for the "GetMap" WMS request
-     */
-    public String buildGetMapUrl(Collection<LayerDetails> selectedLayers, String format) {
-        return buildRootUrl() + "FORMAT=" + format + (imageFormatHasTransparency(format) ? "&TRANSPARENT=TRUE" : "")
-                + "&VERSION=" + version + "&SERVICE=WMS&REQUEST=GetMap&LAYERS="
-                + selectedLayers.stream().map(x -> x.ident).collect(Collectors.joining(","))
-                + "&STYLES=&" + ("1.3.0".equals(version) ? "CRS" : "SRS") + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}";
-    }
-
-    /**
-     * Attempts WMS "GetCapabilities" request and initializes internal variables if successful.
-     * @param serviceUrlStr WMS service URL
-     * @throws IOException if any I/O errors occurs
-     * @throws WMSGetCapabilitiesException if the WMS server replies a ServiceException
-     */
-    public void attemptGetCapabilities(String serviceUrlStr) throws IOException, WMSGetCapabilitiesException {
+     * Returns URL for accessing GetMap service. String will contain following parameters:
+     * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)})
+     * * {width} - that needs to be replaced with width of the tile
+     * * {height} - that needs to be replaces with height of the tile
+     * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates)
+     *
+     * Format of the response will be calculated using {@link #getPreferredFormat()}
+     *
+     * @param selectedLayers list of DefaultLayer selection of layers to be shown
+     * @param transparent whether returned images should contain transparent pixels (if supported by format)
+     * @return URL template for GetMap service containing
+     */
+    public String buildGetMapUrl(List<DefaultLayer> selectedLayers, boolean transparent) {
+        return buildGetMapUrl(
+                getLayers(selectedLayers),
+                selectedLayers.stream().map(x -> x.getStyle()).collect(Collectors.toList()),
+                transparent);
+    }
+
+    /**
+     * @see #buildGetMapUrl(List, boolean)
+     *
+     * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()}
+     * @param selectedStyles selected styles for all selectedLayers
+     * @param transparent whether returned images should contain transparent pixels (if supported by format)
+     * @return URL template for GetMap service
+     */
+    public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) {
+        return buildGetMapUrl(
+                selectedLayers.stream().map(x -> x.getName()).collect(Collectors.toList()),
+                selectedStyles,
+                getPreferredFormat(),
+                transparent);
+    }
+
+    /**
+     * @see #buildGetMapUrl(List, boolean)
+     *
+     * @param selectedLayers selected layers as list of strings
+     * @param selectedStyles selected styles of layers as list of strings
+     * @param format format of the response - one of {@link #getFormats()}
+     * @param transparent whether returned images should contain transparent pixels (if supported by format)
+     * @return URL template for GetMap service
+     */
+    public String buildGetMapUrl(List<String> selectedLayers,
+            Collection<String> selectedStyles,
+            String format,
+            boolean transparent) {
+
+        Utils.ensure(selectedStyles == null || selectedLayers.size() == selectedStyles.size(),
+                tr("Styles size {0} doesn't match layers size {1}"),
+                selectedStyles == null ? 0 : selectedStyles.size(),
+                        selectedLayers.size());
+
+        return buildRootUrl() + "FORMAT=" + format + ((imageFormatHasTransparency(format) && transparent) ? "&TRANSPARENT=TRUE" : "")
+                + "&VERSION=" + this.version + "&SERVICE=WMS&REQUEST=GetMap&LAYERS="
+                + selectedLayers.stream().collect(Collectors.joining(","))
+                + "&STYLES="
+                + (selectedStyles != null ? Utils.join(",", selectedStyles) : "")
+                + "&"
+                + (belowWMS130() ? "SRS" : "CRS")
+                + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}";
+    }
+
+    private boolean tagEquals(QName a, QName b) {
+        boolean ret = a.equals(b);
+        if (ret) {
+            return ret;
+        }
+
+        if (belowWMS130()) {
+            return a.getLocalPart().equals(b.getLocalPart());
+        }
+
+        return false;
+    }
+
+    private void attemptGetCapabilities(String url) throws IOException, WMSGetCapabilitiesException {
+        Logging.debug("Trying WMS getcapabilities with url {0}", url);
+        try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
+                setMaxAge(7 * CachedFile.DAYS).
+                setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
+                getInputStream()) {
+
+            try {
+                XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(in);
+                for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
+                    if (event == XMLStreamReader.START_ELEMENT) {
+                        if (tagEquals(CAPABILITIES_ROOT_111, reader.getName())) {
+                            // version 1.1.1
+                            this.version = reader.getAttributeValue(null, "version");
+                            if (this.version == null) {
+                                this.version = "1.1.1";
+                            }
+                        }
+                        if (tagEquals(CAPABILITITES_ROOT_130, reader.getName())) {
+                            this.version = reader.getAttributeValue(WMS_NS_URL, "version");
+                        }
+                        if (tagEquals(QN_SERVICE, reader.getName())) {
+                            parseService(reader);
+                        }
+
+                        if (tagEquals(QN_CAPABILITY, reader.getName())) {
+                            parseCapability(reader);
+                        }
+                    }
+                }
+            } catch (XMLStreamException e) {
+                String content = new String(cf.getByteContent(), UTF_8);
+                cf.clear(); // if there is a problem with parsing of the file, remove it from the cache
+                throw new WMSGetCapabilitiesException(e, content);
+            }
+        }
+    }
+
+    private void parseService(XMLStreamReader reader) throws XMLStreamException {
+        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_TITLE)) {
+            this.title = reader.getElementText();
+            for (int event = reader.getEventType();
+                    reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_SERVICE, reader.getName()));
+                    event = reader.next()) {
+                // empty loop, just move reader to the end of Service tag, if moveReaderToTag return false, it's already done
+            }
+        }
+    }
+
+    private void parseCapability(XMLStreamReader reader) throws XMLStreamException {
+        for (int event = reader.getEventType();
+                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_CAPABILITY, reader.getName()));
+                event = reader.next()) {
+
+            if (event == XMLStreamReader.START_ELEMENT) {
+                if (tagEquals(QN_REQUEST, reader.getName())) {
+                    parseRequest(reader);
+                }
+                if (tagEquals(QN_LAYER, reader.getName())) {
+                    parseLayer(reader, null);
+                }
+            }
+        }
+    }
+
+    private void parseRequest(XMLStreamReader reader) throws XMLStreamException {
+        String mode = "";
+        String getMapUrl = "";
+        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_GETMAP)) {
+            for (int event = reader.getEventType();
+                    reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_GETMAP, reader.getName()));
+                    event = reader.next()) {
+
+                if (event == XMLStreamReader.START_ELEMENT) {
+                    if (tagEquals(QN_FORMAT, reader.getName())) {
+                        String value = reader.getElementText();
+                        if (isImageFormatSupportedWarn(value) && !this.formats.contains(value)) {
+                            this.formats.add(value);
+                        }
+                    }
+                    if (tagEquals(QN_DCPTYPE, reader.getName()) && GetCapabilitiesParseHelper.moveReaderToTag(reader,
+                            this::tagEquals, QN_HTTP, QN_GET)) {
+                        mode = reader.getName().getLocalPart();
+                        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_ONLINE_RESOURCE)) {
+                            getMapUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
+                        }
+                        // TODO should we handle also POST?
+                        if ("GET".equalsIgnoreCase(mode) && getMapUrl != null && !"".equals(getMapUrl)) {
+                            this.getMapUrl = getMapUrl;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void parseLayer(XMLStreamReader reader, LayerDetails parentLayer) throws XMLStreamException {
+        LayerDetails ret = new LayerDetails(parentLayer);
+        for (int event = reader.next(); // start with advancing reader by one element to get the contents of the layer
+                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_LAYER, reader.getName()));
+                event = reader.next()) {
+
+            if (event == XMLStreamReader.START_ELEMENT) {
+                if (tagEquals(QN_NAME, reader.getName())) {
+                    ret.setName(reader.getElementText());
+                }
+                if (tagEquals(QN_ABSTRACT, reader.getName())) {
+                    ret.setAbstract(GetCapabilitiesParseHelper.getElementTextWithSubtags(reader));
+                }
+                if (tagEquals(QN_TITLE, reader.getName())) {
+                    ret.setTitle(reader.getElementText());
+                }
+                if (tagEquals(QN_CRS, reader.getName())) {
+                    ret.addCrs(reader.getElementText());
+                }
+                if (tagEquals(QN_SRS, reader.getName()) && belowWMS130()) {
+                    ret.addCrs(reader.getElementText());
+                }
+                if (tagEquals(QN_STYLE, reader.getName())) {
+                    parseAndAddStyle(reader, ret);
+                }
+                if (tagEquals(QN_LAYER, reader.getName())) {
+
+                    parseLayer(reader, ret);
+                }
+                if (tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName())) {
+                    if (ret.getBounds() == null) {
+                        Bounds bbox = parseExGeographic(reader);
+                        ret.setBounds(bbox);
+                    }
+
+                }
+                if (tagEquals(QN_BOUNDINGBOX, reader.getName())) {
+                    Projection conv;
+                    if (belowWMS130()) {
+                        conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "SRS"));
+                    } else {
+                        conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "CRS"));
+                    }
+                    if (ret.getBounds() == null && conv != null) {
+                        Bounds bbox = parseBoundingBox(reader, conv);
+                        ret.setBounds(bbox);
+                    }
+                }
+                if (tagEquals(QN_LATLONBOUNDINGBOX, reader.getName()) && belowWMS130()) {
+                    if (ret.getBounds() == null) {
+                        Bounds bbox = parseBoundingBox(reader, null);
+                        ret.setBounds(bbox);
+                    }
+                }
+            }
+        }
+        this.layers.add(ret);
+    }
+
+    /**
+     * @return if this service operates at protocol level below 1.3.0
+     */
+    public boolean belowWMS130() {
+        return this.version.equals("1.1.1") || this.version.equals("1.1") || this.version.equals("1.0");
+    }
+
+    private void parseAndAddStyle(XMLStreamReader reader, LayerDetails ld) throws XMLStreamException {
+        String name = null;
+        String title = null;
+        for (int event = reader.getEventType();
+                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_STYLE, reader.getName()));
+                event = reader.next()) {
+            if (event == XMLStreamReader.START_ELEMENT) {
+                if (tagEquals(QN_NAME, reader.getName())) {
+                    name = reader.getElementText();
+                }
+                if (tagEquals(QN_TITLE, reader.getName())) {
+                    title = reader.getElementText();
+                }
+            }
+        }
+        if (name == null) {
+            name = "";
+        }
+        ld.addStyle(name, title);
+    }
+
+    private Bounds parseExGeographic(XMLStreamReader reader) throws XMLStreamException {
+        String minx = null, maxx = null, maxy = null, miny = null;
+
+        for (int event = reader.getEventType();
+                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()));
+                event = reader.next()) {
+            if (event == XMLStreamReader.START_ELEMENT) {
+                if (tagEquals(QN_WESTBOUNDLONGITUDE, reader.getName())) {
+                    minx = reader.getElementText();
+                }
+
+                if (tagEquals(QN_EASTBOUNDLONGITUDE, reader.getName())) {
+                    maxx = reader.getElementText();
+                }
+
+                if (tagEquals(QN_SOUTHBOUNDLATITUDE, reader.getName())) {
+                    miny = reader.getElementText();
+                }
+
+                if (tagEquals(QN_NORTHBOUNDLATITUDE, reader.getName())) {
+                    maxy = reader.getElementText();
+                }
+            }
+        }
+        return parseBBox(null, miny, minx, maxy, maxx);
+    }
+
+    private Bounds parseBoundingBox(XMLStreamReader reader, Projection conv) {
+        Function<String, String> attrGetter = tag -> belowWMS130() ?
+                reader.getAttributeValue(null, tag)
+                : reader.getAttributeValue(WMS_NS_URL, tag);
+
+                return parseBBox(
+                        conv,
+                        attrGetter.apply("miny"),
+                        attrGetter.apply("minx"),
+                        attrGetter.apply("maxy"),
+                        attrGetter.apply("maxx")
+                        );
+    }
+
+    private Bounds parseBBox(Projection conv, String miny, String minx, String maxy, String maxx) {
+        if (miny == null || minx == null || maxy == null || maxx == null) {
+            return null;
+        }
+        if (conv != null) {
+            new Bounds(
+                    conv.eastNorth2latlon(new EastNorth(getDecimalDegree(minx), getDecimalDegree(miny))),
+                    conv.eastNorth2latlon(new EastNorth(getDecimalDegree(maxx), getDecimalDegree(maxy)))
+                    );
+        }
+        return new Bounds(
+                getDecimalDegree(miny),
+                getDecimalDegree(minx),
+                getDecimalDegree(maxy),
+                getDecimalDegree(maxx)
+                );
+    }
+
+    private static double getDecimalDegree(String value) {
+        // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server)
+        return Double.parseDouble(value.replace(',', '.'));
+    }
+
+
+    private String normalizeUrl(String serviceUrlStr) throws MalformedURLException {
         URL getCapabilitiesUrl = null;
-        try {
-            if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
-                // If the url doesn't already have GetCapabilities, add it in
-                getCapabilitiesUrl = new URL(serviceUrlStr);
-                final String getCapabilitiesQuery = "VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities";
-                if (getCapabilitiesUrl.getQuery() == null) {
-                    getCapabilitiesUrl = new URL(serviceUrlStr + '?' + getCapabilitiesQuery);
-                } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
-                    getCapabilitiesUrl = new URL(serviceUrlStr + '&' + getCapabilitiesQuery);
-                } else {
-                    getCapabilitiesUrl = new URL(serviceUrlStr + getCapabilitiesQuery);
-                }
+        String ret = null;
+
+        if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
+            // If the url doesn't already have GetCapabilities, add it in
+            getCapabilitiesUrl = new URL(serviceUrlStr);
+            ret = serviceUrlStr;
+            if (getCapabilitiesUrl.getQuery() == null) {
+                ret = serviceUrlStr + '?' + CAPABILITIES_QUERY_STRING;
+            } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
+                ret = serviceUrlStr + '&' + CAPABILITIES_QUERY_STRING;
             } else {
-                // Otherwise assume it's a good URL and let the subsequent error
-                // handling systems deal with problems
-                getCapabilitiesUrl = new URL(serviceUrlStr);
-            }
-            // Make sure we don't keep GetCapabilities request in service URL
-            serviceUrl = new URL(serviceUrlStr.replace("REQUEST=GetCapabilities", "").replace("&&", "&"));
-        } catch (HeadlessException e) {
-            Logging.warn(e);
-            return;
-        }
-
-        doAttemptGetCapabilities(serviceUrlStr, getCapabilitiesUrl);
-    }
-
-    /**
-     * Attempts WMS GetCapabilities with version 1.1.1 first, then 1.3.0 in case of specific errors.
-     * @param serviceUrlStr WMS service URL
-     * @param getCapabilitiesUrl GetCapabilities URL
-     * @throws IOException if any I/O error occurs
-     * @throws WMSGetCapabilitiesException if any HTTP or parsing error occurs
-     */
-    private void doAttemptGetCapabilities(String serviceUrlStr, URL getCapabilitiesUrl)
-            throws IOException, WMSGetCapabilitiesException {
-        final String url = getCapabilitiesUrl.toExternalForm();
-        final Response response = HttpClient.create(getCapabilitiesUrl).connect();
-
-        // Is the HTTP connection successul ?
-        if (response.getResponseCode() >= 400) {
-            // HTTP error for servers handling only WMS 1.3.0 ?
-            String errorMessage = response.getResponseMessage();
-            String errorContent = response.fetchContent();
-            Matcher tomcat = HttpClient.getTomcatErrorMatcher(errorContent);
-            boolean messageAbout130 = errorMessage != null && errorMessage.contains("1.3.0");
-            boolean contentAbout130 = errorContent != null && tomcat != null && tomcat.matches() && tomcat.group(1).contains("1.3.0");
-            if (url.contains("VERSION=1.1.1") && (messageAbout130 || contentAbout130)) {
-                doAttemptGetCapabilities130(serviceUrlStr, url);
-                return;
-            }
-            throw new WMSGetCapabilitiesException(errorMessage, errorContent);
-        }
-
-        try {
-            // Parse XML capabilities sent by the server
-            parseCapabilities(serviceUrlStr, response.getContent());
-        } catch (WMSGetCapabilitiesException e) {
-            // ServiceException for servers handling only WMS 1.3.0 ?
-            if (e.getCause() == null && url.contains("VERSION=1.1.1")) {
-                doAttemptGetCapabilities130(serviceUrlStr, url);
-            } else {
-                throw e;
-            }
-        }
-    }
-
-    /**
-     * Attempts WMS GetCapabilities with version 1.3.0.
-     * @param serviceUrlStr WMS service URL
-     * @param url GetCapabilities URL
-     * @throws IOException if any I/O error occurs
-     * @throws WMSGetCapabilitiesException if any HTTP or parsing error occurs
-     * @throws MalformedURLException in case of invalid URL
-     */
-    private void doAttemptGetCapabilities130(String serviceUrlStr, final String url)
-            throws IOException, WMSGetCapabilitiesException {
-        doAttemptGetCapabilities(serviceUrlStr, new URL(url.replace("VERSION=1.1.1", "VERSION=1.3.0")));
-        if (serviceUrl.toExternalForm().contains("VERSION=1.1.1")) {
-            serviceUrl = new URL(serviceUrl.toExternalForm().replace("VERSION=1.1.1", "VERSION=1.3.0"));
-        }
-        version = "1.3.0";
-    }
-
-    void parseCapabilities(String serviceUrlStr, InputStream contentStream) throws IOException, WMSGetCapabilitiesException {
-        String incomingData = null;
-        try {
-            DocumentBuilder builder = Utils.newSafeDOMBuilder();
-            builder.setEntityResolver((publicId, systemId) -> {
-                Logging.info("Ignoring DTD " + publicId + ", " + systemId);
-                return new InputSource(new StringReader(""));
-            });
-            Document document = builder.parse(contentStream);
-            Element root = document.getDocumentElement();
-
-            try {
-                StringWriter writer = new StringWriter();
-                TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document), new StreamResult(writer));
-                incomingData = writer.getBuffer().toString();
-                Logging.debug("Server response to Capabilities request:");
-                Logging.debug(incomingData);
-            } catch (TransformerFactoryConfigurationError | TransformerException e) {
-                Logging.warn(e);
-            }
-
-            // Check if the request resulted in ServiceException
-            if ("ServiceException".equals(root.getTagName())) {
-                throw new WMSGetCapabilitiesException(root.getTextContent(), incomingData);
-            }
-
-            // Some WMS service URLs specify a different base URL for their GetMap service
-            Element child = getChild(root, "Capability");
-            child = getChild(child, "Request");
-            child = getChild(child, "GetMap");
-
-            formats = getChildrenStream(child, "Format")
-                    .map(Node::getTextContent)
-                    .filter(WMSImagery::isImageFormatSupportedWarn)
-                    .collect(Collectors.toList());
-
-            child = getChild(child, "DCPType");
-            child = getChild(child, "HTTP");
-            child = getChild(child, "Get");
-            child = getChild(child, "OnlineResource");
-            if (child != null) {
-                String baseURL = child.getAttributeNS(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
-                if (!baseURL.equals(serviceUrlStr)) {
-                    URL newURL = new URL(baseURL);
-                    if (newURL.getAuthority() != null) {
-                        Logging.info("GetCapabilities specifies a different service URL: " + baseURL);
-                        serviceUrl = newURL;
-                    }
-                }
-            }
-
-            Element capabilityElem = getChild(root, "Capability");
-            List<Element> children = getChildren(capabilityElem, "Layer");
-            layers = parseLayers(children, new HashSet<String>());
-        } catch (MalformedURLException | ParserConfigurationException | SAXException e) {
-            throw new WMSGetCapabilitiesException(e, incomingData);
-        }
+                ret = serviceUrlStr + CAPABILITIES_QUERY_STRING;
+            }
+        } else {
+            // Otherwise assume it's a good URL and let the subsequent error
+            // handling systems deal with problems
+            ret = serviceUrlStr;
+        }
+        return ret;
     }
 
@@ -392,4 +641,5 @@
     }
 
+
     static boolean imageFormatHasTransparency(final String format) {
         return format != null && (format.startsWith("image/png") || format.startsWith("image/gif")
@@ -398,219 +648,58 @@
 
     /**
-     * Returns a new {@code ImageryInfo} describing the given service name and selected WMS layers.
-     * @param name service name
-     * @param selectedLayers selected WMS layers
-     * @return a new {@code ImageryInfo} describing the given service name and selected WMS layers
-     */
-    public ImageryInfo toImageryInfo(String name, Collection<LayerDetails> selectedLayers) {
-        ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers));
-        if (selectedLayers != null) {
-            Set<String> proj = new HashSet<>();
-            for (WMSImagery.LayerDetails l : selectedLayers) {
-                proj.addAll(l.getProjections());
-            }
-            i.setServerProjections(proj);
+     * Creates ImageryInfo object from this GetCapabilities document
+     *
+     * @param name name of imagery layer
+     * @param selectedLayers layers which are to be used by this imagery layer
+     * @param selectedStyles styles that should be used for selectedLayers
+     * @param transparent if layer should be transparent
+     * @return ImageryInfo object
+     */
+    public ImageryInfo toImageryInfo(String name, List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) {
+        ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers, selectedStyles, transparent));
+        if (selectedLayers != null && !selectedLayers.isEmpty()) {
+            i.setServerProjections(getServerProjections(selectedLayers));
         }
         return i;
     }
 
-    private List<LayerDetails> parseLayers(List<Element> children, Set<String> parentCrs) {
-        List<LayerDetails> details = new ArrayList<>(children.size());
-        for (Element element : children) {
-            details.add(parseLayer(element, parentCrs));
-        }
-        return details;
-    }
-
-    private LayerDetails parseLayer(Element element, Set<String> parentCrs) {
-        String name = getChildContent(element, "Title", null, null);
-        String ident = getChildContent(element, "Name", null, null);
-        String abstr = getChildContent(element, "Abstract", null, null);
-
-        // The set of supported CRS/SRS for this layer
-        Set<String> crsList = new HashSet<>();
-        // ...including this layer's already-parsed parent projections
-        crsList.addAll(parentCrs);
-
-        // Parse the CRS/SRS pulled out of this layer's XML element
-        // I think CRS and SRS are the same at this point
-        getChildrenStream(element)
-            .filter(child -> "CRS".equals(child.getNodeName()) || "SRS".equals(child.getNodeName()))
-            .map(WMSImagery::getContent)
-            .filter(crs -> !crs.isEmpty())
-            .map(crs -> crs.trim().toUpperCase(Locale.ENGLISH))
-            .forEach(crsList::add);
-
-        // Check to see if any of the specified projections are supported by JOSM
-        boolean josmSupportsThisLayer = false;
-        for (String crs : crsList) {
-            josmSupportsThisLayer |= isProjSupported(crs);
-        }
-
-        Bounds bounds = null;
-        Element bboxElem = getChild(element, "EX_GeographicBoundingBox");
-        if (bboxElem != null) {
-            // Attempt to use EX_GeographicBoundingBox for bounding box
-            double left = Double.parseDouble(getChildContent(bboxElem, "westBoundLongitude", null, null));
-            double top = Double.parseDouble(getChildContent(bboxElem, "northBoundLatitude", null, null));
-            double right = Double.parseDouble(getChildContent(bboxElem, "eastBoundLongitude", null, null));
-            double bot = Double.parseDouble(getChildContent(bboxElem, "southBoundLatitude", null, null));
-            bounds = new Bounds(bot, left, top, right);
-        } else {
-            // If that's not available, try LatLonBoundingBox
-            bboxElem = getChild(element, "LatLonBoundingBox");
-            if (bboxElem != null) {
-                double left = getDecimalDegree(bboxElem, "minx");
-                double top = getDecimalDegree(bboxElem, "maxy");
-                double right = getDecimalDegree(bboxElem, "maxx");
-                double bot = getDecimalDegree(bboxElem, "miny");
-                bounds = new Bounds(bot, left, top, right);
-            }
-        }
-
-        List<Element> layerChildren = getChildren(element, "Layer");
-        List<LayerDetails> childLayers = parseLayers(layerChildren, crsList);
-
-        return new LayerDetails(name, ident, abstr, crsList, josmSupportsThisLayer, bounds, childLayers);
-    }
-
-    private static double getDecimalDegree(Element elem, String attr) {
-        // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server)
-        return Double.parseDouble(elem.getAttribute(attr).replace(',', '.'));
-    }
-
-    private static boolean isProjSupported(String crs) {
-        return Projections.getProjectionByCode(crs) != null;
-    }
-
-    private static String getChildContent(Element parent, String name, String missing, String empty) {
-        Element child = getChild(parent, name);
-        if (child == null)
-            return missing;
-        else {
-            String content = getContent(child);
-            return (!content.isEmpty()) ? content : empty;
-        }
-    }
-
-    private static String getContent(Element element) {
-        NodeList nl = element.getChildNodes();
-        StringBuilder content = new StringBuilder();
-        for (int i = 0; i < nl.getLength(); i++) {
-            Node node = nl.item(i);
-            switch (node.getNodeType()) {
-                case Node.ELEMENT_NODE:
-                    content.append(getContent((Element) node));
-                    break;
-                case Node.CDATA_SECTION_NODE:
-                case Node.TEXT_NODE:
-                    content.append(node.getNodeValue());
-                    break;
-                default: // Do nothing
-            }
-        }
-        return content.toString().trim();
-    }
-
-    private static Stream<Element> getChildrenStream(Element parent) {
-        if (parent == null) {
-            // ignore missing elements
-            return Stream.empty();
-        } else {
-            Iterable<Element> it = () -> new ChildIterator(parent);
-            return StreamSupport.stream(it.spliterator(), false);
-        }
-    }
-
-    private static Stream<Element> getChildrenStream(Element parent, String name) {
-        return getChildrenStream(parent).filter(child -> name.equals(child.getNodeName()));
-    }
-
-    private static List<Element> getChildren(Element parent, String name) {
-        return getChildrenStream(parent, name).collect(Collectors.toList());
-    }
-
-    private static Element getChild(Element parent, String name) {
-        return getChildrenStream(parent, name).findFirst().orElse(null);
-    }
-
-    /**
-     * The details of a layer of this WMS server.
-     */
-    public static class LayerDetails {
-
-        /**
-         * The layer name (WMS {@code Title})
-         */
-        public final String name;
-        /**
-         * The layer ident (WMS {@code Name})
-         */
-        public final String ident;
-        /**
-         * The layer abstract (WMS {@code Abstract})
-         * @since 13199
-         */
-        public final String abstr;
-        /**
-         * The child layers of this layer
-         */
-        public final List<LayerDetails> children;
-        /**
-         * The bounds this layer can be used for
-         */
-        public final Bounds bounds;
-        /**
-         * the CRS/SRS pulled out of this layer's XML element
-         */
-        public final Set<String> crsList;
-        /**
-         * {@code true} if any of the specified projections are supported by JOSM
-         */
-        public final boolean supported;
-
-        /**
-         * Constructs a new {@code LayerDetails}.
-         * @param name The layer name (WMS {@code Title})
-         * @param ident The layer ident (WMS {@code Name})
-         * @param abstr The layer abstract (WMS {@code Abstract})
-         * @param crsList The CRS/SRS pulled out of this layer's XML element
-         * @param supportedLayer {@code true} if any of the specified projections are supported by JOSM
-         * @param bounds The bounds this layer can be used for
-         * @param childLayers The child layers of this layer
-         * @since 13199
-         */
-        public LayerDetails(String name, String ident, String abstr, Set<String> crsList, boolean supportedLayer, Bounds bounds,
-                List<LayerDetails> childLayers) {
-            this.name = name;
-            this.ident = ident;
-            this.abstr = abstr;
-            this.supported = supportedLayer;
-            this.children = childLayers;
-            this.bounds = bounds;
-            this.crsList = crsList;
-        }
-
-        /**
-         * Determines if any of the specified projections are supported by JOSM.
-         * @return {@code true} if any of the specified projections are supported by JOSM
-         */
-        public boolean isSupported() {
-            return this.supported;
-        }
-
-        /**
-         * Returns the CRS/SRS pulled out of this layer's XML element.
-         * @return the CRS/SRS pulled out of this layer's XML element
-         */
-        public Set<String> getProjections() {
-            return crsList;
-        }
-
-        @Override
-        public String toString() {
-            String baseName = (name == null || name.isEmpty()) ? ident : name;
-            return abstr == null || abstr.equalsIgnoreCase(baseName) ? baseName : baseName + " (" + abstr + ')';
-        }
+    /**
+     * Returns projections that server supports for provided list of layers. This will be intersection of projections
+     * defined for each layer
+     *
+     * @param selectedLayers list of layers
+     * @return projection code
+     */
+    public Collection<String> getServerProjections(List<LayerDetails> selectedLayers) {
+        if (selectedLayers.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Set<String> proj = new HashSet<>(selectedLayers.get(0).getCrs());
+
+        // set intersect with all layers
+        for (LayerDetails ld: selectedLayers) {
+            proj.retainAll(ld.getCrs());
+        }
+        return proj;
+    }
+
+
+    /**
+     * @param defaultLayers
+     * @return collection of LayerDetails specified by DefaultLayers
+     */
+    public List<LayerDetails> getLayers(List<DefaultLayer> defaultLayers) {
+        Collection<String> layerNames = defaultLayers.stream().map(x -> x.getLayerName()).collect(Collectors.toList());
+        return layers.stream()
+                .flatMap(LayerDetails::flattened)
+                .filter(x -> layerNames.contains(x.getName()))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * @return title of this service
+     */
+    public String getTitle() {
+        return title;
     }
 }
