Index: /trunk/.classpath
===================================================================
--- /trunk/.classpath	(revision 3714)
+++ /trunk/.classpath	(revision 3715)
@@ -16,5 +16,5 @@
 	<classpathentry kind="lib" path="test/lib/fest/MRJToolkitStubs-1.0.jar"/>
 	<classpathentry kind="lib" path="test/lib/jfcunit.jar"/>
-	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JDK 6"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
 	<classpathentry exported="true" kind="con" path="GROOVY_SUPPORT"/>
 	<classpathentry kind="lib" path="lib/signpost-core-1.2.1.1.jar"/>
Index: /trunk/src/org/openstreetmap/josm/Main.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/Main.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/Main.java	(revision 3715)
@@ -62,4 +62,5 @@
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
+import org.openstreetmap.josm.gui.preferences.ImageryPreference;
 import org.openstreetmap.josm.gui.preferences.MapPaintPreference;
 import org.openstreetmap.josm.gui.preferences.ProjectionPreference;
@@ -220,4 +221,5 @@
         TaggingPresetPreference.initialize();
         MapPaintPreference.initialize();
+        ImageryPreference.initialize();
 
         validator = new OsmValidator();
Index: /trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java	(revision 3715)
@@ -0,0 +1,26 @@
+package org.openstreetmap.josm.actions;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+
+public class AddImageryLayerAction extends JosmAction {
+
+    private final ImageryInfo info;
+
+    public AddImageryLayerAction(ImageryInfo info) {
+        super(info.getMenuName(), "imagery_menu", tr("Add imagery layer {0}",info.getName()), null, false);
+        putValue("toolbar", "imagery_" + info.getToolbarName());
+        this.info = info;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        ImageryLayer wmsLayer = ImageryLayer.create(info);
+        Main.main.addLayer(wmsLayer);
+    }
+};
Index: /trunk/src/org/openstreetmap/josm/actions/ImageryAdjustAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/ImageryAdjustAction.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/actions/ImageryAdjustAction.java	(revision 3715)
@@ -0,0 +1,182 @@
+package org.openstreetmap.josm.actions;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Cursor;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.text.DecimalFormat;
+
+import javax.swing.JFormattedTextField;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.imagery.OffsetBookmark;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+
+public class ImageryAdjustAction extends MapMode implements MouseListener, MouseMotionListener{
+    static ImageryOffsetDialog offsetDialog;
+    static Cursor cursor = ImageProvider.getCursor("normal", "move");
+
+    double oldDx, oldDy;
+    boolean mouseDown;
+    EastNorth prevEastNorth;
+    private ImageryLayer layer;
+    private MapMode oldMapMode;
+
+    public ImageryAdjustAction(ImageryLayer layer) {
+        super(tr("New offset"), "adjustimg",
+                tr("Adjust the position of this imagery layer"), Main.map,
+                cursor);
+        this.layer = layer;
+    }
+
+    @Override public void enterMode() {
+        super.enterMode();
+        if (layer == null)
+            return;
+        if (!layer.isVisible()) {
+            layer.setVisible(true);
+        }
+        Main.map.mapView.addMouseListener(this);
+        Main.map.mapView.addMouseMotionListener(this);
+        oldDx = layer.getDx();
+        oldDy = layer.getDy();
+        offsetDialog = new ImageryOffsetDialog();
+        offsetDialog.setVisible(true);
+    }
+
+    @Override public void exitMode() {
+        super.exitMode();
+        if (offsetDialog != null) {
+            layer.setOffset(oldDx, oldDy);
+            offsetDialog.setVisible(false);
+            offsetDialog = null;
+        }
+        Main.map.mapView.removeMouseListener(this);
+        Main.map.mapView.removeMouseMotionListener(this);
+    }
+
+    @Override public void mousePressed(MouseEvent e) {
+        if (e.getButton() != MouseEvent.BUTTON1)
+            return;
+
+        if (layer.isVisible()) {
+            prevEastNorth=Main.map.mapView.getEastNorth(e.getX(),e.getY());
+            Main.map.mapView.setCursor
+            (Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+        }
+    }
+
+    @Override public void mouseDragged(MouseEvent e) {
+        if (layer == null || prevEastNorth == null) return;
+        EastNorth eastNorth =
+            Main.map.mapView.getEastNorth(e.getX(),e.getY());
+        double dx = layer.getDx()+eastNorth.east()-prevEastNorth.east();
+        double dy = layer.getDy()+eastNorth.north()-prevEastNorth.north();
+        layer.setOffset(dx, dy);
+        if (offsetDialog != null) {
+            offsetDialog.updateOffset();
+        }
+        Main.map.repaint();
+        prevEastNorth = eastNorth;
+    }
+
+    @Override public void mouseReleased(MouseEvent e) {
+        Main.map.mapView.repaint();
+        Main.map.mapView.setCursor(Cursor.getDefaultCursor());
+        prevEastNorth = null;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        if (offsetDialog != null || layer == null || Main.map == null)
+            return;
+        oldMapMode = Main.map.mapMode;
+        super.actionPerformed(e);
+    }
+
+
+    class ImageryOffsetDialog extends ExtendedDialog implements PropertyChangeListener {
+        public final JFormattedTextField easting = new JFormattedTextField(new DecimalFormat("0.00000E0"));
+        public final JFormattedTextField northing = new JFormattedTextField(new DecimalFormat("0.00000E0"));
+        JTextField tBookmarkName = new JTextField();
+        private boolean ignoreListener;
+        public ImageryOffsetDialog() {
+            super(Main.parent,
+                    tr("Adjust imagery offset"),
+                    new String[] { tr("OK"),tr("Cancel") },
+                    false);
+            setButtonIcons(new String[] { "ok", "cancel" });
+            contentInsets = new Insets(15, 15, 5, 15);
+            JPanel pnl = new JPanel();
+            pnl.setLayout(new GridBagLayout());
+            pnl.add(new JLabel(tr("Easting") + ": "),GBC.std());
+            pnl.add(easting,GBC.std().fill(GBC.HORIZONTAL).insets(0, 0, 5, 0));
+            pnl.add(new JLabel(tr("Northing") + ": "),GBC.std());
+            pnl.add(northing,GBC.eol());
+            pnl.add(new JLabel(tr("Bookmark name: ")),GBC.eol().insets(0,5,0,0));
+            pnl.add(tBookmarkName,GBC.eol().fill(GBC.HORIZONTAL));
+            easting.setColumns(8);
+            northing.setColumns(8);
+            easting.setValue(layer.getDx());
+            northing.setValue(layer.getDy());
+            easting.addPropertyChangeListener("value",this);
+            northing.addPropertyChangeListener("value",this);
+            setContent(pnl);
+            setupDialog();
+        }
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            if (ignoreListener) return;
+            layer.setOffset(((Number)easting.getValue()).doubleValue(), ((Number)northing.getValue()).doubleValue());
+            Main.map.repaint();
+        }
+
+        public void updateOffset() {
+            ignoreListener = true;
+            easting.setValue(layer.getDx());
+            northing.setValue(layer.getDy());
+            ignoreListener = false;
+        }
+
+        @Override
+        protected void buttonAction(int buttonIndex, ActionEvent evt) {
+            super.buttonAction(buttonIndex, evt);
+            offsetDialog = null;
+            if (buttonIndex == 1) {
+                layer.setOffset(oldDx, oldDy);
+            } else if (tBookmarkName.getText() != null && !"".equals(tBookmarkName.getText())) {
+                OffsetBookmark b = new OffsetBookmark(
+                        Main.proj,layer.getInfo().getName(),
+                        tBookmarkName.getText(),
+                        layer.getDx(),layer.getDy());
+                OffsetBookmark.allBookmarks.add(b);
+                OffsetBookmark.saveBookmarks();
+            }
+            Main.main.menu.imageryMenuUpdater.refreshOffsetMenu();
+            if (Main.map == null) return;
+            if (oldMapMode != null) {
+                Main.map.selectMapMode(oldMapMode);
+                oldMapMode = null;
+            } else {
+                Main.map.selectSelectTool(false);
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/actions/Map_Rectifier_WMSmenuAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/Map_Rectifier_WMSmenuAction.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/actions/Map_Rectifier_WMSmenuAction.java	(revision 3715)
@@ -0,0 +1,238 @@
+package org.openstreetmap.josm.actions;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JTextField;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Shortcut;
+import org.openstreetmap.josm.tools.UrlLabel;
+
+public class Map_Rectifier_WMSmenuAction extends JosmAction {
+    /**
+     * Class that bundles all required information of a rectifier service
+     */
+    public static class RectifierService {
+        private final String name;
+        private final String url;
+        private final String wmsUrl;
+        private final Pattern urlRegEx;
+        private final Pattern idValidator;
+        public JRadioButton btn;
+        /**
+         * @param name: Name of the rectifing service
+         * @param url: URL to the service where users can register, upload, etc.
+         * @param wmsUrl: URL to the WMS server where JOSM will grab the images. Insert __s__ where the ID should be placed
+         * @param urlRegEx: a regular expression that determines if a given URL is one of the service and returns the WMS id if so
+         * @param idValidator: regular expression that checks if a given ID is syntactically valid
+         */
+        public RectifierService(String name, String url, String wmsUrl, String urlRegEx, String idValidator) {
+            this.name = name;
+            this.url = url;
+            this.wmsUrl = wmsUrl;
+            this.urlRegEx = Pattern.compile(urlRegEx);
+            this.idValidator = Pattern.compile(idValidator);
+        }
+
+        public boolean isSelected() {
+            return btn.isSelected();
+        }
+    }
+
+    /**
+     * List of available rectifier services. May be extended from the outside
+     */
+    public ArrayList<RectifierService> services = new ArrayList<RectifierService>();
+
+    public Map_Rectifier_WMSmenuAction() {
+        super(tr("Rectified Image..."),
+                "OLmarker",
+                tr("Download Rectified Images From Various Services"),
+                Shortcut.registerShortcut("wms:rectimg",
+                        tr("WMS: {0}", tr("Rectified Image...")),
+                        KeyEvent.VK_R,
+                        Shortcut.GROUP_NONE),
+                        true
+        );
+
+        // Add default services
+        services.add(
+                new RectifierService("Metacarta Map Rectifier",
+                        "http://labs.metacarta.com/rectifier/",
+                        "http://labs.metacarta.com/rectifier/wms.cgi?id=__s__&srs=EPSG:4326"
+                        + "&Service=WMS&Version=1.1.0&Request=GetMap&format=image/png&",
+                        // This matches more than the "classic" WMS link, so users can pretty much
+                        // copy any link as long as it includes the ID
+                        "labs\\.metacarta\\.com/(?:.*?)(?:/|=)([0-9]+)(?:\\?|/|\\.|$)",
+                "^[0-9]+$")
+        );
+        services.add(
+                // TODO: Change all links to mapwarper.net once the project has moved.
+                // The RegEx already matches the new URL and old URLs will be forwarded
+                // to make the transition as smooth as possible for the users
+                new RectifierService("Geothings Map Warper",
+                        "http://warper.geothings.net/",
+                        "http://warper.geothings.net/maps/wms/__s__?request=GetMap&version=1.1.1"
+                        + "&styles=&format=image/png&srs=epsg:4326&exceptions=application/vnd.ogc.se_inimage&",
+                        // This matches more than the "classic" WMS link, so users can pretty much
+                        // copy any link as long as it includes the ID
+                        "(?:mapwarper\\.net|warper\\.geothings\\.net)/(?:.*?)/([0-9]+)(?:\\?|/|\\.|$)",
+                "^[0-9]+$")
+        );
+
+        // This service serves the purpose of "just this once" without forcing the user
+        // to commit the link to the preferences
+
+        // Clipboard content gets trimmed, so matching whitespace only ensures that this
+        // service will never be selected automatically.
+        services.add(new RectifierService(tr("Custom WMS Link"), "", "", "^\\s+$", ""));
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        JPanel panel = new JPanel(new GridBagLayout());
+        panel.add(new JLabel(tr("Supported Rectifier Services:")), GBC.eol());
+
+        JTextField tfWmsUrl = new JTextField(30);
+
+        String clip = getClipboardContents();
+        ButtonGroup group = new ButtonGroup();
+
+        JRadioButton firstBtn = null;
+        for(RectifierService s : services) {
+            JRadioButton serviceBtn = new JRadioButton(s.name);
+            if(firstBtn == null)
+                firstBtn = serviceBtn;
+            // Checks clipboard contents against current service if no match has been found yet.
+            // If the contents match, they will be inserted into the text field and the corresponding
+            // service will be pre-selected.
+            if(!clip.equals("") && tfWmsUrl.getText().equals("")
+                    && (s.urlRegEx.matcher(clip).find() || s.idValidator.matcher(clip).matches())) {
+                serviceBtn.setSelected(true);
+                tfWmsUrl.setText(clip);
+            }
+            s.btn = serviceBtn;
+            group.add(serviceBtn);
+            if(!s.url.equals("")) {
+                panel.add(serviceBtn, GBC.std());
+                panel.add(new UrlLabel(s.url, tr("Visit Homepage")), GBC.eol().anchor(GridBagConstraints.EAST));
+            } else
+                panel.add(serviceBtn, GBC.eol().anchor(GridBagConstraints.WEST));
+        }
+
+        // Fallback in case no match was found
+        if(tfWmsUrl.getText().equals("") && firstBtn != null)
+            firstBtn.setSelected(true);
+
+        panel.add(new JLabel(tr("WMS URL or Image ID:")), GBC.eol());
+        panel.add(tfWmsUrl, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+
+        ExtendedDialog diag = new ExtendedDialog(Main.parent,
+                tr("Add Rectified Image"),
+
+                new String[] {tr("Add Rectified Image"), tr("Cancel")});
+        diag.setContent(panel);
+        diag.setButtonIcons(new String[] {"OLmarker.png", "cancel.png"});
+
+        // This repeatedly shows the dialog in case there has been an error.
+        // The loop is break;-ed if the users cancels
+        outer: while(true) {
+            diag.showDialog();
+            int answer = diag.getValue();
+            // Break loop when the user cancels
+            if(answer != 1)
+                break;
+
+            String text = tfWmsUrl.getText().trim();
+            // Loop all services until we find the selected one
+            for(RectifierService s : services) {
+                if(!s.isSelected())
+                    continue;
+
+                // We've reached the custom WMS URL service
+                // Just set the URL and hope everything works out
+                if(s.wmsUrl.equals("")) {
+                    addWMSLayer(s.name + " (" + text + ")", text);
+                    break outer;
+                }
+
+                // First try to match if the entered string as an URL
+                Matcher m = s.urlRegEx.matcher(text);
+                if(m.find()) {
+                    String id = m.group(1);
+                    String newURL = s.wmsUrl.replaceAll("__s__", id);
+                    String title = s.name + " (" + id + ")";
+                    addWMSLayer(title, newURL);
+                    break outer;
+                }
+                // If not, look if it's a valid ID for the selected service
+                if(s.idValidator.matcher(text).matches()) {
+                    String newURL = s.wmsUrl.replaceAll("__s__", text);
+                    String title = s.name + " (" + text + ")";
+                    addWMSLayer(title, newURL);
+                    break outer;
+                }
+
+                // We've found the selected service, but the entered string isn't suitable for
+                // it. So quit checking the other radio buttons
+                break;
+            }
+
+            // and display an error message. The while(true) ensures that the dialog pops up again
+            JOptionPane.showMessageDialog(Main.parent,
+                    tr("Couldn't match the entered link or id to the selected service. Please try again."),
+                    tr("No valid WMS URL or id"),
+                    JOptionPane.ERROR_MESSAGE);
+            diag.setVisible(true);
+        }
+    }
+
+    /**
+     * Adds a WMS Layer with given title and URL
+     * @param title: Name of the layer as it will shop up in the layer manager
+     * @param url: URL to the WMS server
+     */
+    private void addWMSLayer(String title, String url) {
+        Main.main.addLayer(new WMSLayer(new ImageryInfo(title, url)));
+    }
+
+    /**
+     * Helper function that extracts a String from the Clipboard if available.
+     * Returns an empty String otherwise
+     * @return String Clipboard contents if available
+     */
+    private String getClipboardContents() {
+        String result = "";
+        Transferable contents = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
+
+        if(contents == null || !contents.isDataFlavorSupported(DataFlavor.stringFlavor))
+            return "";
+
+        try {
+            result = (String)contents.getTransferData(DataFlavor.stringFlavor);
+        } catch(Exception ex) {
+            return "";
+        }
+        return result.trim();
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/imagery/GeorefImage.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/imagery/GeorefImage.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/data/imagery/GeorefImage.java	(revision 3715)
@@ -0,0 +1,247 @@
+package org.openstreetmap.josm.data.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.Transparency;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.ref.SoftReference;
+
+import javax.imageio.ImageIO;
+
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.gui.NavigatableComponent;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
+
+public class GeorefImage implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    public enum State { IMAGE, NOT_IN_CACHE, FAILED};
+
+    private WMSLayer layer;
+    private State state;
+
+    private BufferedImage image;
+    private SoftReference<BufferedImage> reImg;
+    private int xIndex;
+    private int yIndex;
+
+    private static final Color transparentColor = new Color(0,0,0,0);
+    private Color fadeColor = transparentColor;
+
+    public EastNorth getMin() {
+        return layer.getEastNorth(xIndex, yIndex);
+    }
+
+    public EastNorth getMax() {
+        return layer.getEastNorth(xIndex+1, yIndex+1);
+    }
+
+
+    public GeorefImage(WMSLayer layer) {
+        this.layer = layer;
+    }
+
+    public void changePosition(int xIndex, int yIndex) {
+        if (!equalPosition(xIndex, yIndex)) {
+            this.xIndex = xIndex;
+            this.yIndex = yIndex;
+            this.image = null;
+            flushedResizedCachedInstance();
+        }
+    }
+
+    public boolean equalPosition(int xIndex, int yIndex) {
+        return this.xIndex == xIndex && this.yIndex == yIndex;
+    }
+
+    public void changeImage(State state, BufferedImage image) {
+        flushedResizedCachedInstance();
+        this.image = image;
+        this.state = state;
+
+        switch (state) {
+        case FAILED:
+        {
+            BufferedImage img = createImage();
+            layer.drawErrorTile(img);
+            this.image = img;
+            break;
+        }
+        case NOT_IN_CACHE:
+        {
+            BufferedImage img = createImage();
+            Graphics g = img.getGraphics();
+            g.setColor(Color.GRAY);
+            g.fillRect(0, 0, img.getWidth(), img.getHeight());
+            Font font = g.getFont();
+            Font tempFont = font.deriveFont(Font.PLAIN).deriveFont(36.0f);
+            g.setFont(tempFont);
+            g.setColor(Color.BLACK);
+            g.drawString(tr("Not in cache"), 10, img.getHeight()/2);
+            g.setFont(font);
+            this.image = img;
+            break;
+        }
+        default:
+            this.image = layer.sharpenImage(this.image);
+            break;
+        }
+    }
+
+    private BufferedImage createImage() {
+        return new BufferedImage(layer.getBaseImageWidth(), layer.getBaseImageHeight(), BufferedImage.TYPE_INT_RGB);
+    }
+
+    public boolean paint(Graphics g, NavigatableComponent nc, int xIndex, int yIndex, int leftEdge, int bottomEdge) {
+        if (image == null)
+            return false;
+
+        if(!(this.xIndex == xIndex && this.yIndex == yIndex))
+            return false;
+
+        int left = layer.getImageX(xIndex);
+        int bottom = layer.getImageY(yIndex);
+        int width = layer.getImageWidth(xIndex);
+        int height = layer.getImageHeight(yIndex);
+
+        int x = left - leftEdge;
+        int y = nc.getHeight() - (bottom - bottomEdge) - height;
+
+        // This happens if you zoom outside the world
+        if(width == 0 || height == 0)
+            return false;
+
+        // TODO: implement per-layer fade color
+        Color newFadeColor;
+        if (ImageryLayer.PROP_FADE_AMOUNT.get() == 0) {
+            newFadeColor = transparentColor;
+        } else {
+            newFadeColor = ImageryLayer.getFadeColorWithAlpha();
+        }
+
+        BufferedImage img = reImg == null?null:reImg.get();
+        if(img != null && img.getWidth() == width && img.getHeight() == height && fadeColor.equals(newFadeColor)) {
+            g.drawImage(img, x, y, null);
+            return true;
+        }
+
+        fadeColor = newFadeColor;
+
+        boolean alphaChannel = WMSLayer.PROP_ALPHA_CHANNEL.get() && getImage().getTransparency() != Transparency.OPAQUE;
+
+        try {
+            if(img != null) {
+                img.flush();
+            }
+            long freeMem = Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory();
+            //System.out.println("Free Memory:           "+ (freeMem/1024/1024) +" MB");
+            // Notice that this value can get negative due to integer overflows
+            //System.out.println("Img Size:              "+ (width*height*3/1024/1024) +" MB");
+
+            int multipl = alphaChannel ? 4 : 3;
+            // This happens when requesting images while zoomed out and then zooming in
+            // Storing images this large in memory will certainly hang up JOSM. Luckily
+            // traditional rendering is as fast at these zoom levels, so it's no loss.
+            // Also prevent caching if we're out of memory soon
+            if(width > 2000 || height > 2000 || width*height*multipl > freeMem) {
+                fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);
+            } else {
+                // We haven't got a saved resized copy, so resize and cache it
+                img = new BufferedImage(width, height, alphaChannel?BufferedImage.TYPE_INT_ARGB:BufferedImage.TYPE_3BYTE_BGR);
+                img.getGraphics().drawImage(getImage(),
+                        0, 0, width, height, // dest
+                        0, 0, getImage().getWidth(null), getImage().getHeight(null), // src
+                        null);
+                if (!alphaChannel) {
+                    drawFadeRect(img.getGraphics(), 0, 0, width, height);
+                }
+                img.getGraphics().dispose();
+                g.drawImage(img, x, y, null);
+                reImg = new SoftReference<BufferedImage>(img);
+            }
+        } catch(Exception e) {
+            fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);
+        }
+        return true;
+    }
+
+    private void fallbackDraw(Graphics g, Image img, int x, int y, int width, int height, boolean alphaChannel) {
+        flushedResizedCachedInstance();
+        g.drawImage(
+                img, x, y, x + width, y + height,
+                0, 0, img.getWidth(null), img.getHeight(null),
+                null);
+        if (!alphaChannel) { //FIXME: fading for layers with alpha channel currently is not supported
+            drawFadeRect(g, x, y, width, height);
+        }
+    }
+
+    private void drawFadeRect(Graphics g, int x, int y, int width, int height) {
+        if (fadeColor != transparentColor) {
+            g.setColor(fadeColor);
+            g.fillRect(x, y, width, height);
+        }
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        state = (State) in.readObject();
+        boolean hasImage = in.readBoolean();
+        if (hasImage) {
+            image = (ImageIO.read(ImageIO.createImageInputStream(in)));
+        } else {
+            in.readObject(); // read null from input stream
+            image = null;
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        out.writeObject(state);
+        if(getImage() == null) {
+            out.writeBoolean(false);
+            out.writeObject(null);
+        } else {
+            out.writeBoolean(true);
+            ImageIO.write(getImage(), "png", ImageIO.createImageOutputStream(out));
+        }
+    }
+
+    public void flushedResizedCachedInstance() {
+        if (reImg != null) {
+            BufferedImage img = reImg.get();
+            if (img != null) {
+                img.flush();
+            }
+        }
+        reImg = null;
+    }
+
+
+    public BufferedImage getImage() {
+        return image;
+    }
+
+    public State getState() {
+        return state;
+    }
+
+    public int getXIndex() {
+        return xIndex;
+    }
+
+    public int getYIndex() {
+        return yIndex;
+    }
+
+    public void setLayer(WMSLayer layer) {
+        this.layer = layer;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java	(revision 3715)
@@ -0,0 +1,230 @@
+package org.openstreetmap.josm.data.imagery;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Class that stores info about a WMS server.
+ *
+ * @author Frederik Ramm <frederik@remote.org>
+ */
+public class ImageryInfo implements Comparable<ImageryInfo> {
+    public enum ImageryType {
+        WMS("wms"),
+        TMS("tms"),
+        HTML("html"),
+        BING("bing");
+
+        private String urlString;
+        ImageryType(String urlString) {
+            this.urlString = urlString;
+        }
+        public String getUrlString() {
+            return urlString;
+        }
+    }
+
+    String name;
+    String url=null;
+    String cookies = null;
+    public final String eulaAcceptanceRequired;
+    ImageryType imageryType = ImageryType.WMS;
+    double pixelPerDegree = 0.0;
+    int maxZoom = 0;
+
+    public ImageryInfo(String name) {
+        this.name=name;
+        this.eulaAcceptanceRequired = null;
+    }
+
+    public ImageryInfo(String name, String url) {
+        this.name=name;
+        setURL(url);
+        this.eulaAcceptanceRequired = null;
+    }
+
+    public ImageryInfo(String name, String url, String eulaAcceptanceRequired) {
+        this.name=name;
+        setURL(url);
+        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
+    }
+
+    public ImageryInfo(String name, String url, String eulaAcceptanceRequired, String cookies) {
+        this.name=name;
+        setURL(url);
+        this.cookies=cookies;
+        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
+    }
+
+    public ImageryInfo(String name, String url, String cookies, double pixelPerDegree) {
+        this.name=name;
+        setURL(url);
+        this.cookies=cookies;
+        this.pixelPerDegree=pixelPerDegree;
+        this.eulaAcceptanceRequired = null;
+    }
+
+    public ArrayList<String> getInfoArray() {
+        String e2 = null;
+        String e3 = null;
+        String e4 = null;
+        if(url != null && !url.isEmpty()) {
+            e2 = getFullURL();
+        }
+        if(cookies != null && !cookies.isEmpty()) {
+            e3 = cookies;
+        }
+        if(imageryType == ImageryType.WMS || imageryType == ImageryType.HTML) {
+            if(pixelPerDegree != 0.0) {
+                e4 = String.valueOf(pixelPerDegree);
+            }
+        } else {
+            if(maxZoom != 0) {
+                e4 = String.valueOf(maxZoom);
+            }
+        }
+        if(e4 != null && e3 == null) {
+            e3 = "";
+        }
+        if(e3 != null && e2 == null) {
+            e2 = "";
+        }
+
+        ArrayList<String> res = new ArrayList<String>();
+        res.add(name);
+        if(e2 != null) {
+            res.add(e2);
+        }
+        if(e3 != null) {
+            res.add(e3);
+        }
+        if(e4 != null) {
+            res.add(e4);
+        }
+        return res;
+    }
+
+    public ImageryInfo(Collection<String> list) {
+        ArrayList<String> array = new ArrayList<String>(list);
+        this.name=array.get(0);
+        if(array.size() >= 2) {
+            setURL(array.get(1));
+        }
+        if(array.size() >= 3) {
+            this.cookies=array.get(2);
+        }
+        if(array.size() >= 4) {
+            if (imageryType == ImageryType.WMS || imageryType == ImageryType.HTML) {
+                this.pixelPerDegree=Double.valueOf(array.get(3));
+            } else {
+                this.maxZoom=Integer.valueOf(array.get(3));
+            }
+        }
+        this.eulaAcceptanceRequired = null;
+    }
+
+    public ImageryInfo(ImageryInfo i) {
+        this.name=i.name;
+        this.url=i.url;
+        this.cookies=i.cookies;
+        this.imageryType=i.imageryType;
+        this.pixelPerDegree=i.pixelPerDegree;
+        this.eulaAcceptanceRequired = null;
+    }
+
+    @Override
+    public int compareTo(ImageryInfo in)
+    {
+        int i = name.compareTo(in.name);
+        if(i == 0) {
+            i = url.compareTo(in.url);
+        }
+        if(i == 0) {
+            i = Double.compare(pixelPerDegree, in.pixelPerDegree);
+        }
+        return i;
+    }
+
+    public boolean equalsBaseValues(ImageryInfo in)
+    {
+        return url.equals(in.url);
+    }
+
+    public void setPixelPerDegree(double ppd) {
+        this.pixelPerDegree = ppd;
+    }
+
+    public void setMaxZoom(int maxZoom) {
+        this.maxZoom = maxZoom;
+    }
+
+    public void setURL(String url) {
+        for (ImageryType type : ImageryType.values()) {
+            if (url.startsWith(type.getUrlString() + ":")) {
+                this.url = url.substring(type.getUrlString().length() + 1);
+                this.imageryType = type;
+                return;
+            }
+        }
+
+        // Default imagery type is WMS
+        this.url = url;
+        this.imageryType = ImageryType.WMS;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getURL() {
+        return this.url;
+    }
+
+    public String getCookies() {
+        return this.cookies;
+    }
+
+    public double getPixelPerDegree() {
+        return this.pixelPerDegree;
+    }
+
+    public int getMaxZoom() {
+        return this.maxZoom;
+    }
+
+    public String getFullURL() {
+        return imageryType.getUrlString() + ":" + url;
+    }
+
+    public String getToolbarName()
+    {
+        String res = name;
+        if(pixelPerDegree != 0.0) {
+            res += "#PPD="+pixelPerDegree;
+        }
+        return res;
+    }
+
+    public String getMenuName()
+    {
+        String res = name;
+        if(pixelPerDegree != 0.0) {
+            res += " ("+pixelPerDegree+")";
+        } else if(maxZoom != 0) {
+            res += " (z"+maxZoom+")";
+        }
+        return res;
+    }
+
+    public ImageryType getImageryType() {
+        return imageryType;
+    }
+
+    public static boolean isUrlWithPatterns(String url) {
+        return url != null && url.contains("{") && url.contains("}");
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java	(revision 3715)
@@ -0,0 +1,122 @@
+package org.openstreetmap.josm.data.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.io.MirroredInputStream;
+
+public class ImageryLayerInfo {
+    public static final ImageryLayerInfo instance = new ImageryLayerInfo();
+    ArrayList<ImageryInfo> layers = new ArrayList<ImageryInfo>();
+    ArrayList<ImageryInfo> defaultLayers = new ArrayList<ImageryInfo>();
+    private final static String[] DEFAULT_LAYER_SITES
+    = { "http://josm.openstreetmap.de/maps"};
+
+    public void load() {
+        layers.clear();
+        Collection<String> defaults = Main.pref.getCollection(
+                "imagery.layers.default", Collections.<String>emptySet());
+        for(Collection<String> c : Main.pref.getArray("imagery.layers",
+                Collections.<Collection<String>>emptySet())) {
+            layers.add(new ImageryInfo(c));
+        }
+
+        ArrayList<String> defaultsSave = new ArrayList<String>();
+        for(String source : Main.pref.getCollection("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES)))
+        {
+            try
+            {
+                MirroredInputStream s = new MirroredInputStream(source, -1);
+                InputStreamReader r;
+                try
+                {
+                    r = new InputStreamReader(s, "UTF-8");
+                }
+                catch (UnsupportedEncodingException e)
+                {
+                    r = new InputStreamReader(s);
+                }
+                BufferedReader reader = new BufferedReader(r);
+                String line;
+                while((line = reader.readLine()) != null)
+                {
+                    String val[] = line.split(";");
+                    if(!line.startsWith("#") && (val.length == 3 || val.length == 4)) {
+                        boolean force = "true".equals(val[0]);
+                        String name = tr(val[1]);
+                        String url = val[2];
+                        String eulaAcceptanceRequired = null;
+                        if (val.length == 4) {
+                            // 4th parameter optional for license agreement (EULA)
+                            eulaAcceptanceRequired = val[3];
+                        }
+                        defaultLayers.add(new ImageryInfo(name, url, eulaAcceptanceRequired));
+
+                        if(force) {
+                            defaultsSave.add(url);
+                            if(!defaults.contains(url)) {
+                                for(ImageryInfo i : layers) {
+                                    if(url.equals(i.url)) {
+                                        force = false;
+                                    }
+                                }
+                                if(force) {
+                                    layers.add(new ImageryInfo(name, url));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            catch (IOException e)
+            {
+            }
+        }
+
+        Main.pref.putCollection("imagery.layers.default", defaultsSave.size() > 0
+                ? defaultsSave : defaults);
+        Collections.sort(layers);
+        save();
+    }
+
+    public void add(ImageryInfo info) {
+        layers.add(info);
+    }
+
+    public void remove(ImageryInfo info) {
+        layers.remove(info);
+    }
+
+    public void save() {
+        LinkedList<Collection<String>> coll = new LinkedList<Collection<String>>();
+        for (ImageryInfo info : layers) {
+            coll.add(info.getInfoArray());
+        }
+        Main.pref.putArray("imagery.layers", coll);
+    }
+
+    public List<ImageryInfo> getLayers() {
+        return Collections.unmodifiableList(layers);
+    }
+
+    public List<ImageryInfo> getDefaultLayers() {
+        return Collections.unmodifiableList(defaultLayers);
+    }
+
+    public static void addLayer(ImageryInfo info) {
+        instance.add(info);
+        instance.save();
+        Main.main.menu.imageryMenuUpdater.refreshImageryMenu();
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/imagery/OffsetBookmark.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/imagery/OffsetBookmark.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/data/imagery/OffsetBookmark.java	(revision 3715)
@@ -0,0 +1,77 @@
+package org.openstreetmap.josm.data.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+
+public class OffsetBookmark {
+    public static List<OffsetBookmark> allBookmarks = new ArrayList<OffsetBookmark>();
+
+    public Projection proj;
+    public String layerName;
+    public String name;
+    public double dx, dy;
+
+    public boolean isUsable(ImageryLayer layer) {
+        return Main.proj.getClass() == proj.getClass() &&
+        layer.getInfo().getName().equals(layerName);
+    }
+    public OffsetBookmark(Projection proj, String layerName, String name, double dx, double dy) {
+        this.proj = proj;
+        this.layerName = layerName;
+        this.name = name;
+        this.dx = dx;
+        this.dy = dy;
+    }
+
+    public OffsetBookmark(Collection<String> list) {
+        ArrayList<String> array = new ArrayList<String>(list);
+        String projectionName = array.get(0);
+        for (Projection proj : Projection.allProjections) {
+            if (proj.getCacheDirectoryName().equals(projectionName)) {
+                this.proj = proj;
+                break;
+            }
+        }
+        if (this.proj == null)
+            throw new IllegalStateException(tr("Projection ''{0}'' not found", projectionName));
+        this.layerName = array.get(1);
+        this.name = array.get(2);
+        this.dx = Double.valueOf(array.get(3));
+        this.dy = Double.valueOf(array.get(4));
+    }
+
+    public ArrayList<String> getInfoArray() {
+        ArrayList<String> res = new ArrayList<String>(5);
+        res.add(proj.getCacheDirectoryName()); // we should use non-localized projection name
+        res.add(layerName);
+        res.add(name);
+        res.add(String.valueOf(dx));
+        res.add(String.valueOf(dy));
+        return res;
+    }
+
+    public static void loadBookmarks() {
+        for(Collection<String> c : Main.pref.getArray("imagery.offsets",
+                Collections.<Collection<String>>emptySet())) {
+            allBookmarks.add(new OffsetBookmark(c));
+        }
+    }
+
+    public static void saveBookmarks() {
+        LinkedList<Collection<String>> coll = new LinkedList<Collection<String>>();
+        for (OffsetBookmark b : allBookmarks) {
+            coll.add(b.getInfoArray());
+        }
+        Main.pref.putArray("imagery.offsets", coll);
+    }
+
+}
Index: /trunk/src/org/openstreetmap/josm/gui/MainApplication.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 3715)
@@ -221,5 +221,5 @@
         monitor.worked(1);
 
-        if (RemoteControl.on && RemoteControl.PROP_REMOTECONTROL_ENABLED.get()) {
+        if (RemoteControl.PROP_REMOTECONTROL_ENABLED.get()) {
             RemoteControl.start();
         }
@@ -266,6 +266,6 @@
                         dialog.setContent(
                                 trn("JOSM found {0} unsaved osm data layer. ",
-                                "JOSM found {0} unsaved osm data layers. ", unsavedLayerFiles.size(), unsavedLayerFiles.size()) +
-                                tr("It looks like JOSM crashed last time. Do you like to restore the data?"));
+                                        "JOSM found {0} unsaved osm data layers. ", unsavedLayerFiles.size(), unsavedLayerFiles.size()) +
+                                        tr("It looks like JOSM crashed last time. Do you like to restore the data?"));
                         dialog.setButtonIcons(new String[] {"ok", "cancel", "dialogs/remove"});
                         int selection = dialog.showDialog().getValue();
@@ -278,5 +278,5 @@
                     autosaveTask.schedule();
                 }
-                
+
                 main.postConstructorProcessCmdLine(args);
             }
Index: /trunk/src/org/openstreetmap/josm/gui/MainMenu.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/MainMenu.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/gui/MainMenu.java	(revision 3715)
@@ -5,6 +5,10 @@
 import static org.openstreetmap.josm.tools.I18n.marktr;
 import static org.openstreetmap.josm.tools.I18n.tr;
-
+import static org.openstreetmap.josm.tools.I18n.trc;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
+import java.util.List;
 
 import javax.swing.JCheckBoxMenuItem;
@@ -16,4 +20,5 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.AboutAction;
+import org.openstreetmap.josm.actions.AddImageryLayerAction;
 import org.openstreetmap.josm.actions.AddNodeAction;
 import org.openstreetmap.josm.actions.AlignInCircleAction;
@@ -42,4 +47,5 @@
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.actions.JumpToAction;
+import org.openstreetmap.josm.actions.Map_Rectifier_WMSmenuAction;
 import org.openstreetmap.josm.actions.MergeLayerAction;
 import org.openstreetmap.josm.actions.MergeNodesAction;
@@ -85,6 +91,10 @@
 import org.openstreetmap.josm.actions.audio.AudioSlowerAction;
 import org.openstreetmap.josm.actions.search.SearchAction;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
 import org.openstreetmap.josm.gui.io.RecentlyOpenedFilesMenu;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
 import org.openstreetmap.josm.gui.tagging.TaggingPresetSearchAction;
 import org.openstreetmap.josm.tools.Shortcut;
@@ -180,7 +190,8 @@
     public final JMenu toolsMenu = addMenu(marktr("Tools"), KeyEvent.VK_T, 3, ht("/Menu/Tools"));
     public final JMenu presetsMenu = addMenu(marktr("Presets"), KeyEvent.VK_P, 4, ht("/Menu/Presets"));
+    public final JMenu imageryMenu = addMenu(marktr("Imagery"), KeyEvent.VK_I, 5, ht("/Menu/Imagery"));
     public JMenu audioMenu = null;
-    public final JMenu helpMenu = addMenu(marktr("Help"), KeyEvent.VK_H, 5, ht("/Menu/Help"));
-    public final int defaultMenuPos = 5;
+    public final JMenu helpMenu = addMenu(marktr("Help"), KeyEvent.VK_H, 6, ht("/Menu/Help"));
+    public final int defaultMenuPos = 6;
 
     public final JosmAction moveUpAction = new MoveAction(MoveAction.Direction.UP);
@@ -191,5 +202,5 @@
 
     public final TaggingPresetSearchAction presetSearchAction = new TaggingPresetSearchAction();
-
+    public final ImageryMenuUpdater imageryMenuUpdater;
 
     /**
@@ -329,5 +340,5 @@
 
         if (!Main.pref.getBoolean("audio.menuinvisible", false)) {
-            audioMenu = addMenu(marktr("Audio"), KeyEvent.VK_A, 5, ht("/Menu/Audio"));
+            audioMenu = addMenu(marktr("Audio"), KeyEvent.VK_A, defaultMenuPos, ht("/Menu/Audio"));
             add(audioMenu, audioPlayPause);
             add(audioMenu, audioNext);
@@ -347,4 +358,5 @@
 
         new PresetsMenuEnabler(presetsMenu).refreshEnabled();
+        imageryMenuUpdater = new ImageryMenuUpdater();
     }
 
@@ -378,3 +390,88 @@
         }
     }
+
+    public class ImageryMenuUpdater implements MapView.LayerChangeListener {
+        JMenu offsetSubMenu = new JMenu(trc("layer","Offset"));
+
+        public ImageryMenuUpdater() {
+            MapView.addLayerChangeListener(this);
+        }
+
+        public void refreshImageryMenu() {
+            imageryMenu.removeAll();
+
+            // for each configured WMSInfo, add a menu entry.
+            for (final ImageryInfo u : ImageryLayerInfo.instance.getLayers()) {
+                imageryMenu.add(new JMenuItem(new AddImageryLayerAction(u)));
+            }
+            imageryMenu.addSeparator();
+            imageryMenu.add(new JMenuItem(new Map_Rectifier_WMSmenuAction()));
+
+            imageryMenu.addSeparator();
+            imageryMenu.add(offsetSubMenu);
+            imageryMenu.addSeparator();
+            imageryMenu.add(new JMenuItem(new
+                    JosmAction(tr("Blank Layer"), "blankmenu", tr("Open a blank WMS layer to load data from a file"), null, false) {
+                @Override
+                public void actionPerformed(ActionEvent ev) {
+                    Main.main.addLayer(new WMSLayer());
+                }
+            }));
+            refreshEnabled();
+        }
+
+        public void refreshEnabled() {
+            imageryMenu.setEnabled(Main.map != null
+                    && Main.map.mapView !=null
+            );
+        }
+
+        public void refreshOffsetMenu() {
+            offsetSubMenu.removeAll();
+            if (Main.map == null || Main.map.mapView == null) {
+                offsetSubMenu.setEnabled(false);
+                return;
+            }
+            List<ImageryLayer> layers = Main.map.mapView.getLayersOfType(ImageryLayer.class);
+            if (layers.isEmpty()) {
+                offsetSubMenu.setEnabled(false);
+                return;
+            }
+            offsetSubMenu.setEnabled(true);
+            if (layers.size() == 1) {
+                for (Component c : layers.get(0).getOffsetMenu()) {
+                    offsetSubMenu.add(c);
+                }
+                return;
+            }
+            for (ImageryLayer layer : layers) {
+                JMenu subMenu = new JMenu(layer.getName());
+                subMenu.setIcon(layer.getIcon());
+                for (Component c : layer.getOffsetMenu()) {
+                    subMenu.add(c);
+                }
+                offsetSubMenu.add(subMenu);
+            }
+        }
+
+        @Override
+        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+        }
+
+        @Override
+        public void layerAdded(Layer newLayer) {
+            if (newLayer instanceof ImageryLayer) {
+                refreshOffsetMenu();
+            }
+            refreshEnabled();
+        }
+
+        @Override
+        public void layerRemoved(Layer oldLayer) {
+            if (oldLayer instanceof ImageryLayer) {
+                refreshOffsetMenu();
+            }
+            refreshEnabled();
+        }
+    }
 }
Index: /trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java	(revision 3715)
@@ -12,4 +12,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Vector;
@@ -30,5 +32,8 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
 import org.openstreetmap.josm.data.preferences.StringProperty;
+import org.openstreetmap.josm.gui.layer.TMSLayer;
 
 public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser{
@@ -65,6 +70,6 @@
 
         @Override public Image getAttributionImage() { return source.getAttributionImage(); }
- 
-        @Override public String getAttributionText(int zoom, LatLon topLeft, LatLon botRight) { return source.getAttributionText(zoom, topLeft, botRight); }
+
+        @Override public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) { return source.getAttributionText(zoom, topLeft, botRight); }
 
         @Override public boolean requiresAttribution() { return source.requiresAttribution(); }
@@ -74,4 +79,38 @@
         @Override public String getTermsOfUseURL() { return source.getTermsOfUseURL(); }
     }
+
+    /**
+     * TMS TileSource provider for the slippymap chooser
+     */
+    public static class TMSTileSourceProvider implements TileSourceProvider {
+        static final HashSet<String> existingSlippyMapUrls = new HashSet<String>();
+        static {
+            // Urls that already exist in the slippymap chooser and shouldn't be copied from TMS layer list
+            existingSlippyMapUrls.add("http://tile.openstreetmap.org/");
+            existingSlippyMapUrls.add("http://tah.openstreetmap.org/Tiles/tile/");
+            existingSlippyMapUrls.add("http://tile.opencyclemap.org/cycle/");
+        }
+
+        @Override
+        public List<TileSource> getTileSources() {
+            if (!TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()) return Collections.<TileSource>emptyList();
+            List<TileSource> sources = new ArrayList<TileSource>();
+            for (ImageryInfo info : ImageryLayerInfo.instance.getLayers()) {
+                if (existingSlippyMapUrls.contains(info.getURL())) {
+                    continue;
+                }
+                TileSource source = TMSLayer.getTileSource(info);
+                if (source != null) {
+                    sources.add(source);
+                }
+            }
+            return sources;
+        }
+
+        public static void addExistingSlippyMapUrl(String url) {
+            existingSlippyMapUrls.add(url);
+        }
+    }
+
 
     /**
@@ -96,4 +135,5 @@
             }
         });
+        addTileSourceProvider(new TMSTileSourceProvider());
     }
 
Index: /trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java	(revision 3715)
@@ -0,0 +1,208 @@
+package org.openstreetmap.josm.gui.layer;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trc;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.image.BufferedImage;
+import java.awt.image.BufferedImageOp;
+import java.awt.image.ConvolveOp;
+import java.awt.image.Kernel;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.Icon;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JSeparator;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ImageryAdjustAction;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.OffsetBookmark;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+public abstract class ImageryLayer extends Layer {
+    protected static final Icon icon = ImageProvider.get("imagery_small");
+
+    public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0);
+    public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
+
+    public static Color getFadeColor() {
+        return Main.pref.getColor("imagery.fade", Color.white);
+    }
+
+    public static Color getFadeColorWithAlpha() {
+        Color c = getFadeColor();
+        return new Color(c.getRed(),c.getGreen(),c.getBlue(),PROP_FADE_AMOUNT.get()*255/100);
+    }
+
+    public static void setFadeColor(Color color) {
+        Main.pref.putColor("imagery.fade", color);
+    }
+
+    protected final ImageryInfo info;
+    protected MapView mv;
+
+    protected double dx = 0.0;
+    protected double dy = 0.0;
+
+    protected int sharpenLevel;
+
+    public ImageryLayer(ImageryInfo info) {
+        super(info.getName());
+        this.info = info;
+        this.mv = Main.map.mapView;
+        this.sharpenLevel = PROP_SHARPEN_LEVEL.get();
+    }
+
+    public double getPPD(){
+        ProjectionBounds bounds = mv.getProjectionBounds();
+        return mv.getWidth() / (bounds.max.east() - bounds.min.east());
+    }
+
+    public double getDx() {
+        return dx;
+    }
+
+    public double getDy() {
+        return dy;
+    }
+
+    public void setOffset(double dx, double dy) {
+        this.dx = dx;
+        this.dy = dy;
+    }
+
+    public void displace(double dx, double dy) {
+        setOffset(this.dx += dx, this.dy += dy);
+    }
+
+    public ImageryInfo getInfo() {
+        return info;
+    }
+
+    @Override
+    public Icon getIcon() {
+        return icon;
+    }
+
+    @Override
+    public boolean isMergable(Layer other) {
+        return false;
+    }
+
+    @Override
+    public void mergeFrom(Layer from) {
+    }
+
+    @Override
+    public Object getInfoComponent() {
+        return getToolTipText();
+    }
+
+    public static ImageryLayer create(ImageryInfo info) {
+        if (info.getImageryType() == ImageryType.WMS || info.getImageryType() == ImageryType.HTML)
+            return new WMSLayer(info);
+        else if (info.getImageryType() == ImageryType.TMS || info.getImageryType() == ImageryType.BING)
+            return new TMSLayer(info);
+        else throw new AssertionError();
+    }
+
+    class ApplyOffsetAction extends AbstractAction {
+        private OffsetBookmark b;
+        ApplyOffsetAction(OffsetBookmark b) {
+            super(b.name);
+            this.b = b;
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            setOffset(b.dx, b.dy);
+            Main.map.repaint();
+            if (!(ev.getSource() instanceof Component)) return;
+            Component source = (Component)ev.getSource();
+            if (source.getParent() == null) return;
+            Container m = source.getParent();
+            for (Component c : m.getComponents()) {
+                if (c instanceof JCheckBoxMenuItem && c != source) {
+                    ((JCheckBoxMenuItem)c).setSelected(false);
+                }
+            }
+        }
+    }
+
+    public class OffsetAction extends AbstractAction implements LayerAction {
+        @Override
+        public void actionPerformed(ActionEvent e) {
+        }
+        @Override
+        public Component createMenuComponent() {
+            JMenu menu = new JMenu(trc("layer", "Offset"));
+            menu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
+            for (Component item : getOffsetMenu()) {
+                menu.add(item);
+            }
+            return menu;
+        }
+        @Override
+        public boolean supportLayers(List<Layer> layers) {
+            return false;
+        }
+    }
+
+    public List<Component> getOffsetMenu() {
+        List<Component> result = new ArrayList<Component>();
+        result.add(new JMenuItem(new ImageryAdjustAction(this)));
+        if (OffsetBookmark.allBookmarks.isEmpty()) return result;
+
+        result.add(new JSeparator(JSeparator.HORIZONTAL));
+        for (OffsetBookmark b : OffsetBookmark.allBookmarks) {
+            if (!b.isUsable(this)) {
+                continue;
+            }
+            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
+            if (b.dx == dx && b.dy == dy) {
+                item.setSelected(true);
+            }
+            result.add(item);
+        }
+        return result;
+    }
+
+    public BufferedImage sharpenImage(BufferedImage img) {
+        if (sharpenLevel <= 0) return img;
+        int width = img.getWidth(null);
+        int height = img.getHeight(null);
+        BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+        tmp.getGraphics().drawImage(img, 0, 0, null);
+        Kernel kernel;
+        if (sharpenLevel == 1) {
+            kernel = new Kernel(2, 2, new float[] { 4, -1, -1, -1});
+        } else {
+            kernel = new Kernel(3, 3, new float[] { -1, -1, -1, -1, 9, -1, -1, -1, -1});
+        }
+        BufferedImageOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
+        return op.filter(tmp, null);
+    }
+
+    public void drawErrorTile(BufferedImage img) {
+        Graphics g = img.getGraphics();
+        g.setColor(Color.RED);
+        g.fillRect(0, 0, img.getWidth(), img.getHeight());
+        g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(36.0f));
+        g.setColor(Color.BLACK);
+        g.drawString(tr("ERROR"), 30, img.getHeight()/2);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 3715)
@@ -0,0 +1,1282 @@
+package org.openstreetmap.josm.gui.layer;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.font.TextAttribute;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ImageObserver;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.gui.jmapviewer.BingAerialTileSource;
+import org.openstreetmap.gui.jmapviewer.Coordinate;
+import org.openstreetmap.gui.jmapviewer.JobDispatcher;
+import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
+import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader;
+import org.openstreetmap.gui.jmapviewer.TMSTileSource;
+import org.openstreetmap.gui.jmapviewer.TemplatedTMSTileSource;
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.RenameLayerAction;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
+import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
+import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+
+/**
+ * Class that displays a slippy map layer.
+ *
+ * @author Frederik Ramm <frederik@remote.org>
+ * @author LuVar <lubomir.varga@freemap.sk>
+ * @author Dave Hansen <dave@sr71.net>
+ *
+ */
+public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener {
+    public static final String PREFERENCE_PREFIX   = "imagery.tms";
+
+    public static final int MAX_ZOOM = 30;
+    public static final int MIN_ZOOM = 2;
+    public static final int DEFAULT_MAX_ZOOM = 18;
+    public static final int DEFAULT_MIN_ZOOM = 2;
+
+    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
+    public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
+    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM);
+    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
+    public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
+    public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
+
+    boolean debug = false;
+    void out(String s)
+    {
+        Main.debug(s);
+    }
+
+    protected MemoryTileCache tileCache;
+    protected TileSource tileSource;
+    protected TileLoader tileLoader;
+    JobDispatcher jobDispatcher = JobDispatcher.getInstance();
+
+    HashSet<Tile> tileRequestsOutstanding = new HashSet<Tile>();
+    @Override
+    public synchronized void tileLoadingFinished(Tile tile, boolean success)
+    {
+        if (!success) {
+            BufferedImage img = new BufferedImage(tileSource.getTileSize(),tileSource.getTileSize(), BufferedImage.TYPE_INT_RGB);
+            drawErrorTile(img);
+            tile.setImage(img);
+        }
+        tile.setLoaded(true);
+        needRedraw = true;
+        Main.map.repaint(100);
+        tileRequestsOutstanding.remove(tile);
+        if (sharpenLevel != 0 && success) {
+            tile.setImage(sharpenImage(tile.getImage()));
+        }
+        if (debug) {
+            out("tileLoadingFinished() tile: " + tile + " success: " + success);
+        }
+    }
+    @Override
+    public TileCache getTileCache()
+    {
+        return tileCache;
+    }
+    void clearTileCache()
+    {
+        if (debug) {
+            out("clearing tile storage");
+        }
+        tileCache = new MemoryTileCache();
+        tileCache.setCacheSize(200);
+    }
+
+    /**
+     * Actual zoom lvl. Initial zoom lvl is set to
+     */
+    public int currentZoomLevel;
+
+    private Tile clickedTile;
+    private boolean needRedraw;
+    private JPopupMenu tileOptionMenu;
+    JCheckBoxMenuItem autoZoomPopup;
+    JCheckBoxMenuItem autoLoadPopup;
+    Tile showMetadataTile;
+    private Image attrImage;
+    private String attrTermsUrl;
+    private Rectangle attrImageBounds, attrToUBounds;
+    private static final Font ATTR_FONT = new Font("Arial", Font.PLAIN, 10);
+    private static final Font ATTR_LINK_FONT;
+    static {
+        HashMap<TextAttribute, Integer> aUnderline = new HashMap<TextAttribute, Integer>();
+        aUnderline.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
+        ATTR_LINK_FONT = ATTR_FONT.deriveFont(aUnderline);
+    }
+
+    protected boolean autoZoom;
+    protected boolean autoLoad;
+
+    void redraw()
+    {
+        needRedraw = true;
+        Main.map.repaint();
+    }
+
+    static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts)
+    {
+        if(maxZoomLvl > MAX_ZOOM) {
+            System.err.println("MaxZoomLvl shouldnt be more than 30! Setting to 30.");
+            maxZoomLvl = MAX_ZOOM;
+        }
+        if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
+            System.err.println("maxZoomLvl shouldnt be more than minZoomLvl! Setting to minZoomLvl.");
+            maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
+        }
+        if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
+            maxZoomLvl = ts.getMaxZoom();
+        }
+        return maxZoomLvl;
+    }
+
+    public static int getMaxZoomLvl(TileSource ts)
+    {
+        return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
+    }
+
+    public static void setMaxZoomLvl(int maxZoomLvl) {
+        maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null);
+        PROP_MAX_ZOOM_LVL.put(maxZoomLvl);
+    }
+
+    static int checkMinZoomLvl(int minZoomLvl, TileSource ts)
+    {
+        if(minZoomLvl < MIN_ZOOM) {
+            System.err.println("minZoomLvl shouldnt be lees than "+MIN_ZOOM+"! Setting to that.");
+            minZoomLvl = MIN_ZOOM;
+        }
+        if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
+            System.err.println("minZoomLvl shouldnt be more than maxZoomLvl! Setting to maxZoomLvl.");
+            minZoomLvl = getMaxZoomLvl(ts);
+        }
+        if (ts != null && ts.getMinZoom() > minZoomLvl) {
+            System.err.println("increasomg minZoomLvl to match tile source");
+            minZoomLvl = ts.getMinZoom();
+        }
+        return minZoomLvl;
+    }
+
+    public static int getMinZoomLvl(TileSource ts)
+    {
+        return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
+    }
+
+    public static void setMinZoomLvl(int minZoomLvl) {
+        minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
+        PROP_MIN_ZOOM_LVL.put(minZoomLvl);
+    }
+
+    public static TileSource getTileSource(ImageryInfo info) {
+        if (info.getImageryType() == ImageryType.TMS) {
+            if(ImageryInfo.isUrlWithPatterns(info.getURL()))
+                return new TemplatedTMSTileSource(info.getName(), info.getURL(), info.getMaxZoom());
+            else
+                return new TMSTileSource(info.getName(),info.getURL(), info.getMaxZoom());
+        } else if (info.getImageryType() == ImageryType.BING)
+            return new BingAerialTileSource();
+        return null;
+    }
+
+    private void initTileSource(TileSource tileSource)
+    {
+        this.tileSource = tileSource;
+        boolean requireAttr = tileSource.requiresAttribution();
+        if(requireAttr) {
+            attrImage = tileSource.getAttributionImage();
+            if(attrImage == null) {
+                System.out.println("Attribution image was null.");
+            } else {
+                System.out.println("Got an attribution image " + attrImage.getHeight(this) + "x" + attrImage.getWidth(this));
+            }
+
+            attrTermsUrl = tileSource.getTermsOfUseURL();
+        }
+
+        currentZoomLevel = getBestZoom();
+        if (currentZoomLevel > getMaxZoomLvl()) {
+            currentZoomLevel = getMaxZoomLvl();
+        }
+        if (currentZoomLevel < getMinZoomLvl()) {
+            currentZoomLevel = getMinZoomLvl();
+        }
+        clearTileCache();
+        //tileloader = new OsmTileLoader(this);
+        tileLoader = new OsmFileCacheTileLoader(this);
+    }
+
+    @Override
+    public void setOffset(double dx, double dy) {
+        super.setOffset(dx, dy);
+        needRedraw = true;
+    }
+
+    private double getPPDeg() {
+        return mv.getWidth()/(mv.getLatLon(mv.getWidth(), mv.getHeight()/2).lon()-mv.getLatLon(0, mv.getHeight()/2).lon());
+    }
+
+    private int getBestZoom() {
+        double ret = Math.log(getPPDeg()*360/tileSource.getTileSize())/Math.log(2);
+        System.out.println("Detected best zoom " + ret);
+        return (int)Math.round(ret);
+    }
+
+    @SuppressWarnings("serial")
+    public TMSLayer(ImageryInfo info) {
+        super(info);
+
+        setBackgroundLayer(true);
+        this.setVisible(true);
+
+        TileSource source = getTileSource(info);
+        if (source == null)
+            throw new IllegalStateException("cannot create TMSLayer with non-TMS ImageryInfo");
+        initTileSource(source);
+
+        tileOptionMenu = new JPopupMenu();
+
+        autoZoom = PROP_DEFAULT_AUTOZOOM.get();
+        autoZoomPopup = new JCheckBoxMenuItem();
+        autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                autoZoom = !autoZoom;
+            }
+        });
+        autoZoomPopup.setSelected(autoZoom);
+        tileOptionMenu.add(autoZoomPopup);
+
+        autoLoad = PROP_DEFAULT_AUTOLOAD.get();
+        autoLoadPopup = new JCheckBoxMenuItem();
+        autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                autoLoad= !autoLoad;
+            }
+        });
+        autoLoadPopup.setSelected(autoLoad);
+        tileOptionMenu.add(autoLoadPopup);
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                if (clickedTile != null) {
+                    loadTile(clickedTile);
+                    redraw();
+                }
+            }
+        }));
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Show Tile Info")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                out("info tile: " + clickedTile);
+                if (clickedTile != null) {
+                    showMetadataTile = clickedTile;
+                    redraw();
+                }
+            }
+        }));
+
+        /* FIXME
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Request Update")) {
+            public void actionPerformed(ActionEvent ae) {
+                if (clickedTile != null) {
+                    clickedTile.requestUpdate();
+                    redraw();
+                }
+            }
+        }));*/
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Load All Tiles")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                loadAllTiles(true);
+                redraw();
+            }
+        }));
+
+        // increase and decrease commands
+        tileOptionMenu.add(new JMenuItem(
+                new AbstractAction(tr("Increase zoom")) {
+                    @Override
+                    public void actionPerformed(ActionEvent ae) {
+                        increaseZoomLevel();
+                        redraw();
+                    }
+                }));
+
+        tileOptionMenu.add(new JMenuItem(
+                new AbstractAction(tr("Decrease zoom")) {
+                    @Override
+                    public void actionPerformed(ActionEvent ae) {
+                        decreaseZoomLevel();
+                        redraw();
+                    }
+                }));
+
+        // FIXME: currently ran in errors
+
+        tileOptionMenu.add(new JMenuItem(
+                new AbstractAction(tr("Snap to tile size")) {
+                    @Override
+                    public void actionPerformed(ActionEvent ae) {
+                        if (lastImageScale == null) {
+                            out("please wait for a tile to be loaded before snapping");
+                            return;
+                        }
+                        double new_factor = Math.sqrt(lastImageScale);
+                        if (debug) {
+                            out("tile snap: scale was: " + lastImageScale + ", new factor: " + new_factor);
+                        }
+                        Main.map.mapView.zoomToFactor(new_factor);
+                        redraw();
+                    }
+                }));
+        // end of adding menu commands
+
+        tileOptionMenu.add(new JMenuItem(
+                new AbstractAction(tr("Flush Tile Cache")) {
+                    @Override
+                    public void actionPerformed(ActionEvent ae) {
+                        System.out.print("flushing all tiles...");
+                        clearTileCache();
+                        System.out.println("done");
+                    }
+                }));
+        // end of adding menu commands
+
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                Main.map.mapView.addMouseListener(new MouseAdapter() {
+                    @Override
+                    public void mouseClicked(MouseEvent e) {
+                        if (e.getButton() == MouseEvent.BUTTON3) {
+                            clickedTile = getTileForPixelpos(e.getX(), e.getY());
+                            tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
+                        } else if (e.getButton() == MouseEvent.BUTTON1) {
+                            if(!tileSource.requiresAttribution())
+                                return;
+
+                            if(attrImageBounds.contains(e.getPoint())) {
+                                try {
+                                    java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
+                                    desktop.browse(new URI(tileSource.getAttributionLinkURL()));
+                                } catch (IOException e1) {
+                                    e1.printStackTrace();
+                                } catch (URISyntaxException e1) {
+                                    e1.printStackTrace();
+                                }
+                            } else if(attrToUBounds.contains(e.getPoint())) {
+                                try {
+                                    java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
+                                    desktop.browse(new URI(tileSource.getTermsOfUseURL()));
+                                } catch (IOException e1) {
+                                    e1.printStackTrace();
+                                } catch (URISyntaxException e1) {
+                                    e1.printStackTrace();
+                                }
+                            }
+                        }
+                    }
+                });
+
+                MapView.addLayerChangeListener(new LayerChangeListener() {
+                    @Override
+                    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+                        //
+                    }
+
+                    @Override
+                    public void layerAdded(Layer newLayer) {
+                        //
+                    }
+
+                    @Override
+                    public void layerRemoved(Layer oldLayer) {
+                        MapView.removeLayerChangeListener(this);
+                    }
+                });
+            }
+        });
+    }
+
+    void zoomChanged()
+    {
+        if (debug) {
+            out("zoomChanged(): " + currentZoomLevel);
+        }
+        needRedraw = true;
+        jobDispatcher.cancelOutstandingJobs();
+        tileRequestsOutstanding.clear();
+    }
+
+    int getMaxZoomLvl()
+    {
+        if (info.getMaxZoom() != 0)
+            return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
+        else
+            return getMaxZoomLvl(tileSource);
+    }
+
+    int getMinZoomLvl()
+    {
+        return getMinZoomLvl(tileSource);
+    }
+
+    /**
+     * Zoom in, go closer to map.
+     *
+     * @return    true, if zoom increasing was successfull, false othervise
+     */
+    public boolean zoomIncreaseAllowed()
+    {
+        boolean zia = currentZoomLevel < this.getMaxZoomLvl();
+        if (debug) {
+            out("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
+        }
+        return zia;
+    }
+    public boolean increaseZoomLevel()
+    {
+        lastImageScale = null;
+        if (zoomIncreaseAllowed()) {
+            currentZoomLevel++;
+            if (debug) {
+                out("increasing zoom level to: " + currentZoomLevel);
+            }
+            zoomChanged();
+        } else {
+            System.err.println("current zoom lvl ("+currentZoomLevel+") couldnt be increased. "+
+                    "MaxZoomLvl ("+this.getMaxZoomLvl()+") reached.");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Zoom out from map.
+     *
+     * @return    true, if zoom increasing was successfull, false othervise
+     */
+    public boolean zoomDecreaseAllowed()
+    {
+        return currentZoomLevel > this.getMinZoomLvl();
+    }
+    public boolean decreaseZoomLevel() {
+        int minZoom = this.getMinZoomLvl();
+        lastImageScale = null;
+        if (zoomDecreaseAllowed()) {
+            if (debug) {
+                out("decreasing zoom level to: " + currentZoomLevel);
+            }
+            currentZoomLevel--;
+            zoomChanged();
+        } else {
+            System.err.println("current zoom lvl couldnt be decreased. MinZoomLvl("+minZoom+") reached.");
+            return false;
+        }
+        return true;
+    }
+
+    /*
+     * We use these for quick, hackish calculations.  They
+     * are temporary only and intentionally not inserted
+     * into the tileCache.
+     */
+    synchronized Tile tempCornerTile(Tile t) {
+        int x = t.getXtile() + 1;
+        int y = t.getYtile() + 1;
+        int zoom = t.getZoom();
+        Tile tile = getTile(x, y, zoom);
+        if (tile != null)
+            return tile;
+        return new Tile(tileSource, x, y, zoom);
+    }
+    synchronized Tile getOrCreateTile(int x, int y, int zoom) {
+        Tile tile = getTile(x, y, zoom);
+        if (tile == null) {
+            tile = new Tile(tileSource, x, y, zoom);
+            tileCache.addTile(tile);
+            tile.loadPlaceholderFromCache(tileCache);
+        }
+        return tile;
+    }
+
+    /*
+     * This can and will return null for tiles that are not
+     * already in the cache.
+     */
+    synchronized Tile getTile(int x, int y, int zoom) {
+        int max = (1 << zoom);
+        if (x < 0 || x >= max || y < 0 || y >= max)
+            return null;
+        Tile tile = tileCache.getTile(tileSource, x, y, zoom);
+        return tile;
+    }
+
+    synchronized boolean loadTile(Tile tile)
+    {
+        if (tile == null)
+            return false;
+        if (tile.hasError())
+            return false;
+        if (tile.isLoaded())
+            return false;
+        if (tile.isLoading())
+            return false;
+        if (tileRequestsOutstanding.contains(tile))
+            return false;
+        tileRequestsOutstanding.add(tile);
+        jobDispatcher.addJob(tileLoader.createTileLoaderJob(tileSource,
+                tile.getXtile(), tile.getYtile(), tile.getZoom()));
+        return true;
+    }
+
+    void loadAllTiles(boolean force) {
+        MapView mv = Main.map.mapView;
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
+
+        TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
+
+        // if there is more than 18 tiles on screen in any direction, do not
+        // load all tiles!
+        if (ts.tooLarge()) {
+            System.out.println("Not downloading all tiles because there is more than 18 tiles on an axis!");
+            return;
+        }
+        ts.loadAllTiles(force);
+    }
+
+    /*
+     * Attempt to approximate how much the image is being scaled. For instance,
+     * a 100x100 image being scaled to 50x50 would return 0.25.
+     */
+    Image lastScaledImage = null;
+    @Override
+    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
+        boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
+        needRedraw = true;
+        if (debug) {
+            out("imageUpdate() done: " + done + " calling repaint");
+        }
+        Main.map.repaint(done ? 0 : 100);
+        return !done;
+    }
+    boolean imageLoaded(Image i) {
+        if (i == null)
+            return false;
+        int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
+        if ((status & ALLBITS) != 0)
+            return true;
+        return false;
+    }
+    Image getLoadedTileImage(Tile tile)
+    {
+        if (!tile.isLoaded())
+            return null;
+        Image img = tile.getImage();
+        if (!imageLoaded(img))
+            return null;
+        return img;
+    }
+
+    double getImageScaling(Image img, Rectangle r) {
+        int realWidth = -1;
+        int realHeight = -1;
+        if (img != null) {
+            realWidth = img.getHeight(this);
+            realWidth = img.getWidth(this);
+        }
+        if (realWidth == -1 || realHeight == -1) {
+            /*
+             * We need a good image against which to work. If
+             * the current one isn't loaded, then try the last one.
+             * Should be good enough. If we've never seen one, then
+             * guess.
+             */
+            if (lastScaledImage != null)
+                return getImageScaling(lastScaledImage, r);
+            realWidth = 256;
+            realHeight = 256;
+        } else {
+            lastScaledImage = img;
+        }
+        /*
+         * If the zoom scale gets really, really off, these can get into
+         * the millions, so make this a double to prevent integer
+         * overflows.
+         */
+        double drawWidth = r.width;
+        double drawHeight = r.height;
+        // stem.out.println("drawWidth: " + drawWidth + " drawHeight: " +
+        // drawHeight);
+
+        double drawArea = drawWidth * drawHeight;
+        double realArea = realWidth * realHeight;
+
+        return drawArea / realArea;
+    }
+
+    LatLon tileLatLon(Tile t)
+    {
+        int zoom = t.getZoom();
+        return new LatLon(tileYToLat(t.getYtile(), zoom),
+                tileXToLon(t.getXtile(), zoom));
+    }
+
+    int paintFromOtherZooms(Graphics g, Tile topLeftTile, Tile botRightTile)
+    {
+        LatLon topLeft  = tileLatLon(topLeftTile);
+        LatLon botRight = tileLatLon(botRightTile);
+
+
+        /*
+         * Go looking for tiles in zoom levels *other* than the current
+         * one. Even if they might look bad, they look better than a
+         * blank tile.
+         *
+         * Make darn sure that the tilesCache can either hold all of
+         * these "fake" tiles or that they don't get inserted in it to
+         * begin with.
+         */
+        //int otherZooms[] = {-5, -4, -3, 2, -2, 1, -1};
+        int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
+        int painted = 0;
+        debug = true;
+        for (int zoomOff : otherZooms) {
+            int zoom = currentZoomLevel + zoomOff;
+            if ((zoom < this.getMinZoomLvl()) ||
+                    (zoom > this.getMaxZoomLvl())) {
+                continue;
+            }
+            TileSet ts = new TileSet(topLeft, botRight, zoom);
+            int zoom_painted = 0;
+            this.paintTileImages(g, ts, zoom, null);
+            if (debug && zoom_painted > 0) {
+                out("painted " + zoom_painted + "/"+ ts.size() +
+                        " tiles from zoom("+zoomOff+"): " + zoom);
+            }
+            painted += zoom_painted;
+            if (zoom_painted >= ts.size()) {
+                if (debug) {
+                    out("broke after drawing " + zoom_painted + "/"+ ts.size() + " at zoomOff: " + zoomOff);
+                }
+                break;
+            }
+        }
+        debug = false;
+        return painted;
+    }
+    Rectangle tileToRect(Tile t1)
+    {
+        /*
+         * We need to get a box in which to draw, so advance by one tile in
+         * each direction to find the other corner of the box.
+         * Note: this somewhat pollutes the tile cache
+         */
+        Tile t2 = tempCornerTile(t1);
+        Rectangle rect = new Rectangle(pixelPos(t1));
+        rect.add(pixelPos(t2));
+        return rect;
+    }
+
+    // 'source' is the pixel coordinates for the area that
+    // the img is capable of filling in.  However, we probably
+    // only want a portion of it.
+    //
+    // 'border' is the screen cordinates that need to be drawn.
+    //  We must not draw outside of it.
+    void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border)
+    {
+        Rectangle target = source;
+
+        // If a border is specified, only draw the intersection
+        // if what we have combined with what we are supposed
+        // to draw.
+        if (border != null) {
+            target = source.intersection(border);
+            if (debug) {
+                out("source: " + source + "\nborder: " + border + "\nintersection: " + target);
+            }
+        }
+
+        // All of the rectangles are in screen coordinates.  We need
+        // to how these correlate to the sourceImg pixels.  We could
+        // avoid doing this by scaling the image up to the 'source' size,
+        // but this should be cheaper.
+        //
+        // In some projections, x any y are scaled differently enough to
+        // cause a pixel or two of fudge.  Calculate them separately.
+        double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
+        double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
+
+        // How many pixels into the 'source' rectangle are we drawing?
+        int screen_x_offset = target.x - source.x;
+        int screen_y_offset = target.y - source.y;
+        // And how many pixels into the image itself does that
+        // correlate to?
+        int img_x_offset = (int)(screen_x_offset * imageXScaling);
+        int img_y_offset = (int)(screen_y_offset * imageYScaling);
+        // Now calculate the other corner of the image that we need
+        // by scaling the 'target' rectangle's dimensions.
+        int img_x_end   = img_x_offset + (int)(target.getWidth() * imageXScaling);
+        int img_y_end   = img_y_offset + (int)(target.getHeight() * imageYScaling);
+
+        if (debug) {
+            out("drawing image into target rect: " + target);
+        }
+        g.drawImage(sourceImg,
+                target.x, target.y,
+                target.x + target.width, target.y + target.height,
+                img_x_offset, img_y_offset,
+                img_x_end, img_y_end,
+                this);
+        if (PROP_FADE_AMOUNT.get() != 0) {
+            // dimm by painting opaque rect...
+            g.setColor(getFadeColorWithAlpha());
+            g.fillRect(target.x, target.y,
+                    target.width, target.height);
+        }
+    }
+    Double lastImageScale = null;
+    // This function is called for several zoom levels, not just
+    // the current one.  It should not trigger any tiles to be
+    // downloaded.  It should also avoid polluting the tile cache
+    // with any tiles since these tiles are not mandatory.
+    //
+    // The "border" tile tells us the boundaries of where we may
+    // draw.  It will not be from the zoom level that is being
+    // drawn currently.  If drawing the currentZoomLevel,
+    // border is null and we draw the entire tile set.
+    List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
+        Rectangle borderRect = null;
+        if (border != null) {
+            borderRect = tileToRect(border);
+        }
+        List<Tile> missedTiles = new LinkedList<Tile>();
+        boolean imageScaleRecorded = false;
+        for (Tile tile : ts.allTiles()) {
+            Image img = getLoadedTileImage(tile);
+            if (img == null) {
+                if (debug) {
+                    out("missed tile: " + tile);
+                }
+                missedTiles.add(tile);
+                continue;
+            }
+            Rectangle sourceRect = tileToRect(tile);
+            if (borderRect != null && !sourceRect.intersects(borderRect)) {
+                continue;
+            }
+            drawImageInside(g, img, sourceRect, borderRect);
+            if (!imageScaleRecorded && zoom == currentZoomLevel) {
+                lastImageScale = new Double(getImageScaling(img, sourceRect));
+                imageScaleRecorded = true;
+            }
+        }// end of for
+        return missedTiles;
+    }
+
+    void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
+        int fontHeight = g.getFontMetrics().getHeight();
+        if (tile == null)
+            return;
+        Point p = pixelPos(t);
+        int texty = p.y + 2 + fontHeight;
+
+        if (PROP_DRAW_DEBUG.get()) {
+            g.drawString("x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
+            texty += 1 + fontHeight;
+            if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
+                g.drawString("x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
+                texty += 1 + fontHeight;
+            }
+        }// end of if draw debug
+
+        if (tile == showMetadataTile) {
+            String md = tile.toString();
+            if (md != null) {
+                g.drawString(md, p.x + 2, texty);
+                texty += 1 + fontHeight;
+            }
+        }
+
+        String tileStatus = tile.getStatus();
+        if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
+            g.drawString(tr("image " + tileStatus), p.x + 2, texty);
+            texty += 1 + fontHeight;
+        }
+
+        int xCursor = -1;
+        int yCursor = -1;
+        if (PROP_DRAW_DEBUG.get()) {
+            if (yCursor < t.getYtile()) {
+                if (t.getYtile() % 32 == 31) {
+                    g.fillRect(0, p.y - 1, mv.getWidth(), 3);
+                } else {
+                    g.drawLine(0, p.y, mv.getWidth(), p.y);
+                }
+                yCursor = t.getYtile();
+            }
+            // This draws the vertical lines for the entire
+            // column. Only draw them for the top tile in
+            // the column.
+            if (xCursor < t.getXtile()) {
+                if (t.getXtile() % 32 == 0) {
+                    // level 7 tile boundary
+                    g.fillRect(p.x - 1, 0, 3, mv.getHeight());
+                } else {
+                    g.drawLine(p.x, 0, p.x, mv.getHeight());
+                }
+                xCursor = t.getXtile();
+            }
+        }
+    }
+
+    private Point pixelPos(LatLon ll) {
+        return Main.map.mapView.getPoint(Main.proj.latlon2eastNorth(ll).add(getDx(), getDy()));
+    }
+    private Point pixelPos(Tile t) {
+        double lon = tileXToLon(t.getXtile(), t.getZoom());
+        LatLon tmpLL = new LatLon(tileYToLat(t.getYtile(), t.getZoom()), lon);
+        return pixelPos(tmpLL);
+    }
+    private LatLon getShiftedLatLon(EastNorth en) {
+        return Main.proj.eastNorth2latlon(en.add(-getDx(), -getDy()));
+    }
+    private Coordinate getShiftedCoord(EastNorth en) {
+        LatLon ll = getShiftedLatLon(en);
+        return new Coordinate(ll.lat(),ll.lon());
+    }
+    private class TileSet {
+        int z12x0, z12x1, z12y0, z12y1;
+        int zoom;
+        int tileMax = -1;
+
+        /**
+         * Create a TileSet by EastNorth bbox taking a layer shift in account
+         */
+        TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
+            this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
+        }
+
+        /**
+         * Create a TileSet by known LatLon bbox without layer shift correction
+         */
+        TileSet(LatLon topLeft, LatLon botRight, int zoom) {
+            this.zoom = zoom;
+
+            z12x0 = lonToTileX(topLeft.lon(),  zoom);
+            z12y0 = latToTileY(topLeft.lat(),  zoom);
+            z12x1 = lonToTileX(botRight.lon(), zoom);
+            z12y1 = latToTileY(botRight.lat(), zoom);
+            if (z12x0 > z12x1) {
+                int tmp = z12x0;
+                z12x0 = z12x1;
+                z12x1 = tmp;
+            }
+            if (z12y0 > z12y1) {
+                int tmp = z12y0;
+                z12y0 = z12y1;
+                z12y1 = tmp;
+            }
+            tileMax = (int)Math.pow(2.0, zoom);
+            if (z12x0 < 0) {
+                z12x0 = 0;
+            }
+            if (z12y0 < 0) {
+                z12y0 = 0;
+            }
+            if (z12x1 > tileMax) {
+                z12x1 = tileMax;
+            }
+            if (z12y1 > tileMax) {
+                z12y1 = tileMax;
+            }
+        }
+        boolean tooSmall() {
+            return this.tilesSpanned() < 2.1;
+        }
+        boolean tooLarge() {
+            return this.tilesSpanned() > 10;
+        }
+        boolean insane() {
+            return this.tilesSpanned() > 100;
+        }
+        double tilesSpanned() {
+            return Math.sqrt(1.0 * this.size());
+        }
+
+        double size() {
+            double x_span = z12x1 - z12x0 + 1.0;
+            double y_span = z12y1 - z12y0 + 1.0;
+            return x_span * y_span;
+        }
+
+        /*
+         * Get all tiles represented by this TileSet that are
+         * already in the tileCache.
+         */
+        List<Tile> allTiles()
+        {
+            return this.allTiles(false);
+        }
+        private List<Tile> allTiles(boolean create)
+        {
+            List<Tile> ret = new ArrayList<Tile>();
+            // Don't even try to iterate over the set.
+            // Someone created a crazy number of them
+            if (this.insane())
+                return ret;
+            for (int x = z12x0; x <= z12x1; x++) {
+                for (int y = z12y0; y <= z12y1; y++) {
+                    Tile t;
+                    if (create) {
+                        t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
+                    } else {
+                        t = getTile(x % tileMax, y % tileMax, zoom);
+                    }
+                    if (t != null) {
+                        ret.add(t);
+                    }
+                }
+            }
+            return ret;
+        }
+        void loadAllTiles(boolean force)
+        {
+            List<Tile> tiles = this.allTiles(true);
+            if (!autoLoad && !force)
+                return;
+            int nr_queued = 0;
+            for (Tile t : tiles) {
+                if (loadTile(t)) {
+                    nr_queued++;
+                }
+            }
+            if (debug)
+                if (nr_queued > 0) {
+                    out("queued to load: " + nr_queued + "/" + tiles.size() + " tiles at zoom: " + zoom);
+                }
+        }
+    }
+
+    boolean az_disable = false;
+    boolean autoZoomEnabled()
+    {
+        if (az_disable)
+            return false;
+        return autoZoom;
+    }
+    /**
+     */
+    @Override
+    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
+        //long start = System.currentTimeMillis();
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
+
+        if (botRight.east() == 0.0 || botRight.north() == 0) {
+            Main.debug("still initializing??");
+            // probably still initializing
+            return;
+        }
+
+        needRedraw = false;
+
+        int zoom = currentZoomLevel;
+        TileSet ts = new TileSet(topLeft, botRight, zoom);
+
+        if (autoZoomEnabled()) {
+            if (zoomDecreaseAllowed() && ts.tooLarge()) {
+                if (debug) {
+                    out("too many tiles, decreasing zoom from " + currentZoomLevel);
+                }
+                if (decreaseZoomLevel()) {
+                    this.paint(g, mv, bounds);
+                }
+                return;
+            }
+            if (zoomIncreaseAllowed() && ts.tooSmall()) {
+                if (debug) {
+                    out("too zoomed in, (" + ts.tilesSpanned()
+                            + "), increasing zoom from " + currentZoomLevel);
+                }
+                // This is a hack.  ts.tooSmall() is proabably a bad thing, and this works
+                // around it.  If we have a very small window, the tileSet may be well
+                // less than 1 real tile wide, but that's expected.  But, this sees the
+                // tile set as too small and zooms in.  The code below that checks for
+                // pixel stretching disagrees and tries to zoom out.  Both calls recurse,
+                // hillarity ensues, and the stack overflows.
+                //
+                // This really needs to get fixed properly.  We probably shouldn't even
+                // have the tooSmall() check on tileSets.  But, this also helps the zoom
+                // converge to the correct place much faster.
+                boolean tmp = az_disable;
+                az_disable = true;
+                if (increaseZoomLevel()) {
+                    this.paint(g, mv, bounds);
+                }
+                az_disable = tmp;
+                return;
+            }
+        }
+
+        // Too many tiles... refuse to draw
+        if (!ts.tooLarge()) {
+            //out("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
+            ts.loadAllTiles(false);
+        }
+
+        g.setColor(Color.DARK_GRAY);
+
+        List<Tile> missedTiles = this.paintTileImages(g, ts, currentZoomLevel, null);
+        int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
+        for (int zoomOffset : otherZooms) {
+            if (!autoZoomEnabled()) {
+                break;
+            }
+            if (!autoLoad) {
+                break;
+            }
+            int newzoom = currentZoomLevel + zoomOffset;
+            if (missedTiles.size() <= 0) {
+                break;
+            }
+            List<Tile> newlyMissedTiles = new LinkedList<Tile>();
+            for (Tile missed : missedTiles) {
+                Tile t2 = tempCornerTile(missed);
+                LatLon topLeft2  = tileLatLon(missed);
+                LatLon botRight2 = tileLatLon(t2);
+                TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
+                if (ts2.tooLarge()) {
+                    continue;
+                }
+                newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
+            }
+            missedTiles = newlyMissedTiles;
+        }
+        if (debug && missedTiles.size() > 0) {
+            out("still missed "+missedTiles.size()+" in the end");
+        }
+        g.setColor(Color.red);
+
+        // The current zoom tileset is guaranteed to have all of
+        // its tiles
+        for (Tile t : ts.allTiles()) {
+            this.paintTileText(ts, t, g, mv, currentZoomLevel, t);
+        }
+
+        if (tileSource.requiresAttribution()) {
+            // Draw attribution
+            Font font = g.getFont();
+            g.setFont(ATTR_LINK_FONT);
+
+            // Draw terms of use text
+            Rectangle2D termsStringBounds = g.getFontMetrics().getStringBounds("Background Terms of Use", g);
+            int textHeight = (int) termsStringBounds.getHeight() - 5;
+            int textWidth = (int) termsStringBounds.getWidth();
+            int termsTextY = mv.getHeight() - textHeight;
+            if(attrTermsUrl != null) {
+                int x = 2;
+                int y = mv.getHeight() - textHeight;
+                attrToUBounds = new Rectangle(x, y, textWidth, textHeight);
+                g.setColor(Color.black);
+                g.drawString("Background Terms of Use", x+1, y+1);
+                g.setColor(Color.white);
+                g.drawString("Background Terms of Use", x, y);
+            }
+
+            // Draw attribution logo
+            int imgWidth = attrImage.getWidth(this);
+            if(attrImage != null) {
+                int x = 2;
+                int height = attrImage.getHeight(this);
+                int y = termsTextY - height - textHeight - 5;
+                attrImageBounds = new Rectangle(x, y, imgWidth, height);
+                g.drawImage(attrImage, x, y, this);
+            }
+
+            g.setFont(ATTR_FONT);
+            String attributionText = tileSource.getAttributionText(currentZoomLevel,
+                    getShiftedCoord(topLeft), getShiftedCoord(botRight));
+            Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
+            {
+                int x = mv.getWidth() - (int) stringBounds.getWidth();
+                int y = mv.getHeight() - textHeight;
+                g.setColor(Color.black);
+                g.drawString(attributionText, x+1, y+1);
+                g.setColor(Color.white);
+                g.drawString(attributionText, x, y);
+            }
+
+            g.setFont(font);
+        }
+
+        if (autoZoomEnabled() && lastImageScale != null) {
+            // If each source image pixel is being stretched into > 3
+            // drawn pixels, zoom in... getting too pixelated
+            if (lastImageScale > 3 && zoomIncreaseAllowed()) {
+                if (debug) {
+                    out("autozoom increase: scale: " + lastImageScale);
+                }
+                increaseZoomLevel();
+                this.paint(g, mv, bounds);
+                // If each source image pixel is being squished into > 0.32
+                // of a drawn pixels, zoom out.
+            } else if ((lastImageScale < 0.45) && (lastImageScale > 0) && zoomDecreaseAllowed()) {
+                if (debug) {
+                    out("autozoom decrease: scale: " + lastImageScale);
+                }
+                decreaseZoomLevel();
+                this.paint(g, mv, bounds);
+            }
+        }
+        //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
+        g.setColor(Color.black);
+        if (ts.insane()) {
+            g.drawString("zoom in to load any tiles", 120, 120);
+        } else if (ts.tooLarge()) {
+            g.drawString("zoom in to load more tiles", 120, 120);
+        } else if (ts.tooSmall()) {
+            g.drawString("increase zoom level to see more detail", 120, 120);
+        }
+    }// end of paint method
+
+    /**
+     * This isn't very efficient, but it is only used when the
+     * user right-clicks on the map.
+     */
+    Tile getTileForPixelpos(int px, int py) {
+        if (debug) {
+            out("getTileForPixelpos("+px+", "+py+")");
+        }
+        MapView mv = Main.map.mapView;
+        Point clicked = new Point(px, py);
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
+        int z = currentZoomLevel;
+        TileSet ts = new TileSet(topLeft, botRight, z);
+
+        if (!ts.tooLarge()) {
+            ts.loadAllTiles(false); // make sure there are tile objects for all tiles
+        }
+        Tile clickedTile = null;
+        for (Tile t1 : ts.allTiles()) {
+            Tile t2 = tempCornerTile(t1);
+            Rectangle r = new Rectangle(pixelPos(t1));
+            r.add(pixelPos(t2));
+            if (debug) {
+                out("r: " + r + " clicked: " + clicked);
+            }
+            if (!r.contains(clicked)) {
+                continue;
+            }
+            clickedTile  = t1;
+            break;
+        }
+        if (clickedTile == null)
+            return null;
+        System.out.println("clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
+                " scale: " + lastImageScale + " currentZoomLevel: " + currentZoomLevel);
+        return clickedTile;
+    }
+
+    @Override
+    public Action[] getMenuEntries() {
+        return new Action[] {
+                LayerListDialog.getInstance().createShowHideLayerAction(),
+                LayerListDialog.getInstance().createDeleteLayerAction(),
+                SeparatorLayerAction.INSTANCE,
+                // color,
+                new OffsetAction(),
+                new RenameLayerAction(this.getAssociatedFile(), this),
+                SeparatorLayerAction.INSTANCE,
+                new LayerListPopup.InfoAction(this) };
+    }
+
+    @Override
+    public String getToolTipText() {
+        return null;
+    }
+
+    @Override
+    public void visitBoundingBox(BoundingXYVisitor v) {
+    }
+
+    @Override
+    public boolean isChanged() {
+        return needRedraw;
+    }
+
+    private int latToTileY(double lat, int zoom) {
+        double l = lat / 180 * Math.PI;
+        double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
+        return (int) (Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI);
+    }
+
+    private int lonToTileX(double lon, int zoom) {
+        return (int) (Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0);
+    }
+
+    private double tileYToLat(int y, int zoom) {
+        return Math.atan(Math.sinh(Math.PI
+                - (Math.PI * y / Math.pow(2.0, zoom - 1))))
+                * 180 / Math.PI;
+    }
+
+    private double tileXToLon(int x, int zoom) {
+        return x * 45.0 / Math.pow(2.0, zoom - 3) - 180.0;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 3715)
@@ -0,0 +1,793 @@
+package org.openstreetmap.josm.gui.layer;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.DiskAccessAction;
+import org.openstreetmap.josm.actions.SaveActionBase;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
+import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.imagery.GeorefImage;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
+import org.openstreetmap.josm.data.imagery.GeorefImage.State;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
+import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.io.imagery.Grabber;
+import org.openstreetmap.josm.io.imagery.HTMLGrabber;
+import org.openstreetmap.josm.io.imagery.WMSGrabber;
+import org.openstreetmap.josm.io.imagery.WMSRequest;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * This is a layer that grabs the current screen from an WMS server. The data
+ * fetched this way is tiled and managed to the disc to reduce server load.
+ */
+public class WMSLayer extends ImageryLayer implements PreferenceChangedListener {
+    public static final BooleanProperty PROP_ALPHA_CHANNEL = new BooleanProperty("imagery.wms.alpha_channel", true);
+    public static final IntegerProperty PROP_SIMULTANEOUS_CONNECTIONS = new IntegerProperty("imagery.wms.simultaneousConnections", 3);
+    public static final BooleanProperty PROP_OVERLAP = new BooleanProperty("imagery.wms.overlap", false);
+    public static final IntegerProperty PROP_OVERLAP_EAST = new IntegerProperty("imagery.wms.overlapEast", 14);
+    public static final IntegerProperty PROP_OVERLAP_NORTH = new IntegerProperty("imagery.wms.overlapNorth", 4);
+
+    public int messageNum = 5; //limit for messages per layer
+    protected String resolution;
+    protected int imageSize = 500;
+    protected int dax = 10;
+    protected int day = 10;
+    protected int daStep = 5;
+    protected int minZoom = 3;
+
+    protected GeorefImage[][] images;
+    protected final int serializeFormatVersion = 5;
+    protected boolean autoDownloadEnabled = true;
+    protected boolean settingsChanged;
+    protected ImageryInfo info;
+
+    // Image index boundary for current view
+    private volatile int bminx;
+    private volatile int bminy;
+    private volatile int bmaxx;
+    private volatile int bmaxy;
+    private volatile int leftEdge;
+    private volatile int bottomEdge;
+
+    // Request queue
+    private final List<WMSRequest> requestQueue = new ArrayList<WMSRequest>();
+    private final List<WMSRequest> finishedRequests = new ArrayList<WMSRequest>();
+    private final Lock requestQueueLock = new ReentrantLock();
+    private final Condition queueEmpty = requestQueueLock.newCondition();
+    private final List<Grabber> grabbers = new ArrayList<Grabber>();
+    private final List<Thread> grabberThreads = new ArrayList<Thread>();
+    private int threadCount;
+    private int workingThreadCount;
+    private boolean canceled;
+
+
+    /** set to true if this layer uses an invalid base url */
+    private boolean usesInvalidUrl = false;
+    /** set to true if the user confirmed to use an potentially invalid WMS base url */
+    private boolean isInvalidUrlConfirmed = false;
+
+    public WMSLayer() {
+        this(new ImageryInfo(tr("Blank Layer")));
+    }
+
+    public WMSLayer(ImageryInfo info) {
+        super(info);
+        setBackgroundLayer(true); /* set global background variable */
+        initializeImages();
+        this.info = new ImageryInfo(info);
+        if(this.info.getPixelPerDegree() == 0.0) {
+            this.info.setPixelPerDegree(getPPD());
+        }
+        resolution = mv.getDist100PixelText();
+
+        if(info.getURL() != null) {
+            WMSGrabber.getProjection(info.getURL(), true);
+            startGrabberThreads();
+            if(info.getImageryType() == ImageryType.WMS && !ImageryInfo.isUrlWithPatterns(info.getURL())) {
+                if (!(info.getURL().endsWith("&") || info.getURL().endsWith("?"))) {
+                    if (!confirmMalformedUrl(info.getURL())) {
+                        System.out.println(tr("Warning: WMS layer deactivated because of malformed base url ''{0}''", info.getURL()));
+                        usesInvalidUrl = true;
+                        setName(getName() + tr("(deactivated)"));
+                        return;
+                    } else {
+                        isInvalidUrlConfirmed = true;
+                    }
+                }
+            }
+        }
+
+        Main.pref.addPreferenceChangeListener(this);
+    }
+
+    public void doSetName(String name) {
+        setName(name);
+        info.setName(name);
+    }
+
+    public boolean hasAutoDownload(){
+        return autoDownloadEnabled;
+    }
+
+    @Override
+    public void destroy() {
+        cancelGrabberThreads(false);
+        Main.pref.removePreferenceChangeListener(this);
+    }
+
+    public void initializeImages() {
+        GeorefImage[][] old = images;
+        images = new GeorefImage[dax][day];
+        if (old != null) {
+            for (int i=0; i<old.length; i++) {
+                for (int k=0; k<old[i].length; k++) {
+                    GeorefImage o = old[i][k];
+                    images[modulo(o.getXIndex(),dax)][modulo(o.getYIndex(),day)] = old[i][k];
+                }
+            }
+        }
+        for(int x = 0; x<dax; ++x) {
+            for(int y = 0; y<day; ++y) {
+                if (images[x][y] == null) {
+                    images[x][y]= new GeorefImage(this);
+                }
+            }
+        }
+    }
+
+    @Override public ImageryInfo getInfo() {
+        return info;
+    }
+
+    @Override public String getToolTipText() {
+        if(autoDownloadEnabled)
+            return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolution);
+        else
+            return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolution);
+    }
+
+    private int modulo (int a, int b) {
+        return a % b >= 0 ? a%b : a%b+b;
+    }
+
+    private boolean zoomIsTooBig() {
+        //don't download when it's too outzoomed
+        return info.getPixelPerDegree() / getPPD() > minZoom;
+    }
+
+    @Override public void paint(Graphics2D g, final MapView mv, Bounds b) {
+        if(info.getURL() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
+
+        settingsChanged = false;
+
+        ProjectionBounds bounds = mv.getProjectionBounds();
+        bminx= getImageXIndex(bounds.min.east());
+        bminy= getImageYIndex(bounds.min.north());
+        bmaxx= getImageXIndex(bounds.max.east());
+        bmaxy= getImageYIndex(bounds.max.north());
+
+        leftEdge = (int)(bounds.min.east() * getPPD());
+        bottomEdge = (int)(bounds.min.north() * getPPD());
+
+        if (zoomIsTooBig()) {
+            for(int x = bminx; x<=bmaxx; ++x) {
+                for(int y = bminy; y<=bmaxy; ++y) {
+                    images[modulo(x,dax)][modulo(y,day)].paint(g, mv, x, y, leftEdge, bottomEdge);
+                }
+            }
+        } else {
+            downloadAndPaintVisible(g, mv, false);
+        }
+    }
+
+    protected boolean confirmMalformedUrl(String url) {
+        if (isInvalidUrlConfirmed)
+            return true;
+        String msg  = tr("<html>The base URL<br>"
+                + "''{0}''<br>"
+                + "for this WMS layer does neither end with a ''&'' nor with a ''?''.<br>"
+                + "This is likely to lead to invalid WMS request. You should check your<br>"
+                + "preference settings.<br>"
+                + "Do you want to fetch WMS tiles anyway?",
+                url);
+        String [] options = new String[] {
+                tr("Yes, fetch images"),
+                tr("No, abort")
+        };
+        int ret = JOptionPane.showOptionDialog(
+                Main.parent,
+                msg,
+                tr("Invalid URL?"),
+                JOptionPane.YES_NO_OPTION,
+                JOptionPane.WARNING_MESSAGE,
+                null,
+                options, options[1]
+        );
+        switch(ret) {
+        case JOptionPane.YES_OPTION: return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public void setOffset(double dx, double dy) {
+        super.setOffset(dx, dy);
+        settingsChanged = true;
+    }
+
+    public int getImageXIndex(double coord) {
+        return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize);
+    }
+
+    public int getImageYIndex(double coord) {
+        return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize);
+    }
+
+    public int getImageX(int imageIndex) {
+        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD());
+    }
+
+    public int getImageY(int imageIndex) {
+        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD());
+    }
+
+    public int getImageWidth(int xIndex) {
+        int overlap = (int)(PROP_OVERLAP.get()?PROP_OVERLAP_EAST.get() * imageSize * getPPD() / info.getPixelPerDegree() / 100:0);
+        return getImageX(xIndex + 1) - getImageX(xIndex) + overlap;
+    }
+
+    public int getImageHeight(int yIndex) {
+        int overlap = (int)(PROP_OVERLAP.get()?PROP_OVERLAP_NORTH.get() * imageSize * getPPD() / info.getPixelPerDegree() / 100:0);
+        return getImageY(yIndex + 1) - getImageY(yIndex) + overlap;
+    }
+
+    /**
+     *
+     * @return Size of image in original zoom
+     */
+    public int getBaseImageWidth() {
+        int overlap = (PROP_OVERLAP.get()?PROP_OVERLAP_EAST.get() * imageSize / 100:0);
+        return imageSize + overlap;
+    }
+
+    /**
+     *
+     * @return Size of image in original zoom
+     */
+    public int getBaseImageHeight() {
+        int overlap = (PROP_OVERLAP.get()?PROP_OVERLAP_NORTH.get() * imageSize / 100:0);
+        return imageSize + overlap;
+    }
+
+
+    /**
+     *
+     * @param xIndex
+     * @param yIndex
+     * @return Real EastNorth of given tile. dx/dy is not counted in
+     */
+    public EastNorth getEastNorth(int xIndex, int yIndex) {
+        return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree());
+    }
+
+
+    protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){
+
+        int newDax = dax;
+        int newDay = day;
+
+        if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) {
+            newDax = ((bmaxx - bminx) / daStep + 1) * daStep;
+        }
+
+        if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) {
+            newDay = ((bmaxy - bminy) / daStep + 1) * daStep;
+        }
+
+        if (newDax != dax || newDay != day) {
+            dax = newDax;
+            day = newDay;
+            initializeImages();
+        }
+
+        for(int x = bminx; x<=bmaxx; ++x) {
+            for(int y = bminy; y<=bmaxy; ++y){
+                images[modulo(x,dax)][modulo(y,day)].changePosition(x, y);
+            }
+        }
+
+        gatherFinishedRequests();
+
+        for(int x = bminx; x<=bmaxx; ++x) {
+            for(int y = bminy; y<=bmaxy; ++y){
+                GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
+                if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) {
+                    WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real);
+                    addRequest(request);
+                }
+            }
+        }
+    }
+
+    @Override public void visitBoundingBox(BoundingXYVisitor v) {
+        for(int x = 0; x<dax; ++x) {
+            for(int y = 0; y<day; ++y)
+                if(images[x][y].getImage() != null){
+                    v.visit(images[x][y].getMin());
+                    v.visit(images[x][y].getMax());
+                }
+        }
+    }
+
+    @Override public Action[] getMenuEntries() {
+        return new Action[]{
+                LayerListDialog.getInstance().createActivateLayerAction(this),
+                LayerListDialog.getInstance().createShowHideLayerAction(),
+                LayerListDialog.getInstance().createDeleteLayerAction(),
+                SeparatorLayerAction.INSTANCE,
+                new OffsetAction(),
+                new LoadWmsAction(),
+                new SaveWmsAction(),
+                new BookmarkWmsAction(),
+                SeparatorLayerAction.INSTANCE,
+                new StartStopAction(),
+                new ToggleAlphaAction(),
+                new ChangeResolutionAction(),
+                new ReloadErrorTilesAction(),
+                new DownloadAction(),
+                SeparatorLayerAction.INSTANCE,
+                new LayerListPopup.InfoAction(this)
+        };
+    }
+
+    public GeorefImage findImage(EastNorth eastNorth) {
+        int xIndex = getImageXIndex(eastNorth.east());
+        int yIndex = getImageYIndex(eastNorth.north());
+        GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];
+        if (result.getXIndex() == xIndex && result.getYIndex() == yIndex)
+            return result;
+        else
+            return null;
+    }
+
+    /**
+     *
+     * @param request
+     * @return -1 if request is no longer needed, otherwise priority of request (lower number <=> more important request)
+     */
+    private int getRequestPriority(WMSRequest request) {
+        if (request.getPixelPerDegree() != info.getPixelPerDegree())
+            return -1;
+        if (bminx > request.getXIndex()
+                || bmaxx < request.getXIndex()
+                || bminy > request.getYIndex()
+                || bmaxy < request.getYIndex())
+            return -1;
+
+        EastNorth cursorEastNorth = mv.getEastNorth(mv.lastMEvent.getX(), mv.lastMEvent.getY());
+        int mouseX = getImageXIndex(cursorEastNorth.east());
+        int mouseY = getImageYIndex(cursorEastNorth.north());
+        int dx = request.getXIndex() - mouseX;
+        int dy = request.getYIndex() - mouseY;
+
+        return dx * dx + dy * dy;
+    }
+
+    public WMSRequest getRequest() {
+        requestQueueLock.lock();
+        try {
+            workingThreadCount--;
+            Iterator<WMSRequest> it = requestQueue.iterator();
+            while (it.hasNext()) {
+                WMSRequest item = it.next();
+                int priority = getRequestPriority(item);
+                if (priority == -1) {
+                    it.remove();
+                } else {
+                    item.setPriority(priority);
+                }
+            }
+            Collections.sort(requestQueue);
+
+            EastNorth cursorEastNorth = mv.getEastNorth(mv.lastMEvent.getX(), mv.lastMEvent.getY());
+            int mouseX = getImageXIndex(cursorEastNorth.east());
+            int mouseY = getImageYIndex(cursorEastNorth.north());
+            boolean isOnMouse = requestQueue.size() > 0 && requestQueue.get(0).getXIndex() == mouseX && requestQueue.get(0).getYIndex() == mouseY;
+
+            // If there is only one thread left then keep it in case we need to download other tile urgently
+            while (!canceled &&
+                    (requestQueue.isEmpty() || (!isOnMouse && threadCount - workingThreadCount == 0 && threadCount > 1))) {
+                try {
+                    queueEmpty.await();
+                } catch (InterruptedException e) {
+                    // Shouldn't happen
+                }
+            }
+
+            workingThreadCount++;
+            if (canceled)
+                return null;
+            else
+                return requestQueue.remove(0);
+
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    public void finishRequest(WMSRequest request) {
+        if (request.getState() == null)
+            throw new IllegalArgumentException("Finished request without state");
+        requestQueueLock.lock();
+        try {
+            finishedRequests.add(request);
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    public void addRequest(WMSRequest request) {
+        requestQueueLock.lock();
+        try {
+            if (!requestQueue.contains(request)) {
+                requestQueue.add(request);
+                queueEmpty.signalAll();
+            }
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    public boolean requestIsValid(WMSRequest request) {
+        return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
+    }
+
+    private void gatherFinishedRequests() {
+        requestQueueLock.lock();
+        try {
+            for (WMSRequest request: finishedRequests) {
+                GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
+                if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
+                    img.changeImage(request.getState(), request.getImage());
+                }
+            }
+        } finally {
+            finishedRequests.clear();
+            requestQueueLock.unlock();
+        }
+    }
+
+    public class DownloadAction extends AbstractAction {
+        private static final long serialVersionUID = -7183852461015284020L;
+        public DownloadAction() {
+            super(tr("Download visible tiles"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            if (zoomIsTooBig()) {
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        tr("The requested area is too big. Please zoom in a little, or change resolution"),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE
+                );
+            } else {
+                downloadAndPaintVisible(mv.getGraphics(), mv, true);
+            }
+        }
+    }
+
+    public class ChangeResolutionAction extends AbstractAction {
+        public ChangeResolutionAction() {
+            super(tr("Change resolution"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            initializeImages();
+            resolution = mv.getDist100PixelText();
+            info.setPixelPerDegree(getPPD());
+            settingsChanged = true;
+            mv.repaint();
+        }
+    }
+
+    public class ReloadErrorTilesAction extends AbstractAction {
+        public ReloadErrorTilesAction() {
+            super(tr("Reload erroneous tiles"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            // Delete small files, because they're probably blank tiles.
+            // See https://josm.openstreetmap.de/ticket/2307
+            Grabber.cache.customCleanUp(CacheFiles.CLEAN_SMALL_FILES, 4096);
+
+            for (int x = 0; x < dax; ++x) {
+                for (int y = 0; y < day; ++y) {
+                    GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
+                    if(img.getState() == State.FAILED){
+                        addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true));
+                        mv.repaint();
+                    }
+                }
+            }
+        }
+    }
+
+    public class ToggleAlphaAction extends AbstractAction implements LayerAction {
+        public ToggleAlphaAction() {
+            super(tr("Alpha channel"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
+            boolean alphaChannel = checkbox.isSelected();
+            PROP_ALPHA_CHANNEL.put(alphaChannel);
+
+            // clear all resized cached instances and repaint the layer
+            for (int x = 0; x < dax; ++x) {
+                for (int y = 0; y < day; ++y) {
+                    GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
+                    img.flushedResizedCachedInstance();
+                }
+            }
+            mv.repaint();
+        }
+        @Override
+        public Component createMenuComponent() {
+            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
+            item.setSelected(PROP_ALPHA_CHANNEL.get());
+            return item;
+        }
+        @Override
+        public boolean supportLayers(List<Layer> layers) {
+            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
+        }
+    }
+
+    public class SaveWmsAction extends AbstractAction {
+        public SaveWmsAction() {
+            super(tr("Save WMS layer to file"), ImageProvider.get("save"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            File f = SaveActionBase.createAndOpenSaveFileChooser(
+                    tr("Save WMS layer"), ".wms");
+            try {
+                if (f != null) {
+                    ObjectOutputStream oos = new ObjectOutputStream(
+                            new FileOutputStream(f)
+                    );
+                    oos.writeInt(serializeFormatVersion);
+                    oos.writeInt(dax);
+                    oos.writeInt(day);
+                    oos.writeInt(imageSize);
+                    oos.writeDouble(info.getPixelPerDegree());
+                    oos.writeObject(info.getName());
+                    oos.writeObject(info.getFullURL());
+                    oos.writeObject(images);
+                    oos.close();
+                }
+            } catch (Exception ex) {
+                ex.printStackTrace(System.out);
+            }
+        }
+    }
+
+    public class LoadWmsAction extends AbstractAction {
+        public LoadWmsAction() {
+            super(tr("Load WMS layer from file"), ImageProvider.get("open"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true,
+                    false, tr("Load WMS layer"), "wms");
+            if(fc == null) return;
+            File f = fc.getSelectedFile();
+            if (f == null) return;
+            try
+            {
+                FileInputStream fis = new FileInputStream(f);
+                ObjectInputStream ois = new ObjectInputStream(fis);
+                int sfv = ois.readInt();
+                if (sfv != serializeFormatVersion) {
+                    JOptionPane.showMessageDialog(Main.parent,
+                            tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion),
+                            tr("File Format Error"),
+                            JOptionPane.ERROR_MESSAGE);
+                    return;
+                }
+                autoDownloadEnabled = false;
+                dax = ois.readInt();
+                day = ois.readInt();
+                imageSize = ois.readInt();
+                info.setPixelPerDegree(ois.readDouble());
+                doSetName((String)ois.readObject());
+                info.setURL((String) ois.readObject());
+                images = (GeorefImage[][])ois.readObject();
+                ois.close();
+                fis.close();
+                for (GeorefImage[] imgs : images) {
+                    for (GeorefImage img : imgs) {
+                        if (img != null) {
+                            img.setLayer(WMSLayer.this);
+                        }
+                    }
+                }
+                settingsChanged = true;
+                mv.repaint();
+                if(info.getURL() != null)
+                {
+                    startGrabberThreads();
+                }
+            }
+            catch (Exception ex) {
+                // FIXME be more specific
+                ex.printStackTrace(System.out);
+                JOptionPane.showMessageDialog(Main.parent,
+                        tr("Error loading file"),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE);
+                return;
+            }
+        }
+    }
+    /**
+     * This action will add a WMS layer menu entry with the current WMS layer
+     * URL and name extended by the current resolution.
+     * When using the menu entry again, the WMS cache will be used properly.
+     */
+    public class BookmarkWmsAction extends AbstractAction {
+        public BookmarkWmsAction() {
+            super(tr("Set WMS Bookmark"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            ImageryLayerInfo.addLayer(new ImageryInfo(info));
+        }
+    }
+
+    private class StartStopAction extends AbstractAction implements LayerAction {
+
+        public StartStopAction() {
+            super(tr("Automatic downloading"));
+        }
+
+        @Override
+        public Component createMenuComponent() {
+            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
+            item.setSelected(autoDownloadEnabled);
+            return item;
+        }
+
+        @Override
+        public boolean supportLayers(List<Layer> layers) {
+            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            autoDownloadEnabled = !autoDownloadEnabled;
+            if (autoDownloadEnabled) {
+                mv.repaint();
+            }
+        }
+    }
+
+    private void cancelGrabberThreads(boolean wait) {
+        requestQueueLock.lock();
+        try {
+            canceled = true;
+            for (Grabber grabber: grabbers) {
+                grabber.cancel();
+            }
+            queueEmpty.signalAll();
+        } finally {
+            requestQueueLock.unlock();
+        }
+        if (wait) {
+            for (Thread t: grabberThreads) {
+                try {
+                    t.join();
+                } catch (InterruptedException e) {
+                    // Shouldn't happen
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    private void startGrabberThreads() {
+        int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
+        requestQueueLock.lock();
+        try {
+            canceled = false;
+            grabbers.clear();
+            grabberThreads.clear();
+            for (int i=0; i<threadCount; i++) {
+                Grabber grabber = getGrabber();
+                grabbers.add(grabber);
+                Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
+                t.setDaemon(true);
+                t.start();
+                grabberThreads.add(t);
+            }
+            this.workingThreadCount = grabbers.size();
+            this.threadCount = grabbers.size();
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    @Override
+    public boolean isChanged() {
+        requestQueueLock.lock();
+        try {
+            return !finishedRequests.isEmpty() || settingsChanged;
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    @Override
+    public void preferenceChanged(PreferenceChangeEvent event) {
+        if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey())) {
+            cancelGrabberThreads(true);
+            startGrabberThreads();
+        } else if (
+                event.getKey().equals(PROP_OVERLAP.getKey())
+                || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
+                || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
+            for (int i=0; i<images.length; i++) {
+                for (int k=0; k<images[i].length; k++) {
+                    images[i][k] = new GeorefImage(this);
+                }
+            }
+
+            settingsChanged = true;
+        }
+    }
+
+    protected Grabber getGrabber(){
+        if(getInfo().getImageryType() == ImageryType.HTML)
+            return new HTMLGrabber(mv, this, Grabber.cache);
+        else if(getInfo().getImageryType() == ImageryType.WMS)
+            return new WMSGrabber(mv, this, Grabber.cache);
+        else throw new IllegalStateException("getGrabber() called for non-WMS layer type");
+    }
+
+}
Index: /trunk/src/org/openstreetmap/josm/gui/preferences/AddWMSLayerPanel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/preferences/AddWMSLayerPanel.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/gui/preferences/AddWMSLayerPanel.java	(revision 3715)
@@ -0,0 +1,530 @@
+package org.openstreetmap.josm.gui.preferences;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.HeadlessException;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.JTree;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.MutableTreeNode;
+import javax.swing.tree.TreePath;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.projection.ProjectionSubPrefs;
+import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
+import org.openstreetmap.josm.tools.GBC;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+
+public class AddWMSLayerPanel extends JPanel {
+    private List<LayerDetails> selectedLayers;
+    private URL serviceUrl;
+    private LayerDetails selectedLayer;
+
+    private JTextField menuName;
+    private JTextArea resultingLayerField;
+    private MutableTreeNode treeRootNode;
+    private DefaultTreeModel treeData;
+    private JTree layerTree;
+    private JButton showBoundsButton;
+
+    private boolean previouslyShownUnsupportedCrsError = false;
+
+    public AddWMSLayerPanel() {
+        JPanel wmsFetchPanel = new JPanel(new GridBagLayout());
+        menuName = new JTextField(40);
+        menuName.setText(tr("Unnamed WMS Layer"));
+        final JTextArea serviceUrl = new JTextArea(3, 40);
+        serviceUrl.setLineWrap(true);
+        serviceUrl.setText("http://sample.com/wms?");
+        wmsFetchPanel.add(new JLabel(tr("Menu Name")), GBC.std().insets(0,0,5,0));
+        wmsFetchPanel.add(menuName, GBC.eop().insets(5,0,0,0).fill(GridBagConstraints.HORIZONTAL));
+        wmsFetchPanel.add(new JLabel(tr("Service URL")), GBC.std().insets(0,0,5,0));
+        JScrollPane scrollPane = new JScrollPane(serviceUrl,
+                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
+                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+        wmsFetchPanel.add(scrollPane, GBC.eop().insets(5,0,0,0));
+        JButton getLayersButton = new JButton(tr("Get Layers"));
+        getLayersButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                Cursor beforeCursor = getCursor();
+                try {
+                    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+                    attemptGetCapabilities(serviceUrl.getText());
+                } finally {
+                    setCursor(beforeCursor);
+                }
+            }
+        });
+        wmsFetchPanel.add(getLayersButton, GBC.eop().anchor(GridBagConstraints.EAST));
+
+        treeRootNode = new DefaultMutableTreeNode();
+        treeData = new DefaultTreeModel(treeRootNode);
+        layerTree = new JTree(treeData);
+        layerTree.setCellRenderer(new LayerTreeCellRenderer());
+        layerTree.addTreeSelectionListener(new TreeSelectionListener() {
+
+            @Override
+            public void valueChanged(TreeSelectionEvent e) {
+                TreePath[] selectionRows = layerTree.getSelectionPaths();
+                if(selectionRows == null) {
+                    showBoundsButton.setEnabled(false);
+                    selectedLayer = null;
+                    return;
+                }
+
+                selectedLayers = new LinkedList<LayerDetails>();
+                for (TreePath i : selectionRows) {
+                    Object userObject = ((DefaultMutableTreeNode) i.getLastPathComponent()).getUserObject();
+                    if(userObject instanceof LayerDetails) {
+                        LayerDetails detail = (LayerDetails) userObject;
+                        if(!detail.isSupported()) {
+                            layerTree.removeSelectionPath(i);
+                            if(!previouslyShownUnsupportedCrsError) {
+                                JOptionPane.showMessageDialog(null, tr("That layer does not support any of JOSM's projections,\n" +
+                                "so you can not use it. This message will not show again."),
+                                tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+                                previouslyShownUnsupportedCrsError = true;
+                            }
+                        } else if(detail.ident != null) {
+                            selectedLayers.add(detail);
+                        }
+                    }
+                }
+
+                if (!selectedLayers.isEmpty()) {
+                    resultingLayerField.setText(buildGetMapUrl());
+
+                    if(selectedLayers.size() == 1) {
+                        showBoundsButton.setEnabled(true);
+                        selectedLayer = selectedLayers.get(0);
+                    }
+                } else {
+                    showBoundsButton.setEnabled(false);
+                    selectedLayer = null;
+                }
+            }
+        });
+        wmsFetchPanel.add(new JScrollPane(layerTree), GBC.eop().insets(5,0,0,0).fill(GridBagConstraints.HORIZONTAL));
+
+        JPanel layerManipulationButtons = new JPanel();
+        showBoundsButton = new JButton(tr("Show Bounds"));
+        showBoundsButton.setEnabled(false);
+        showBoundsButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                if(selectedLayer.bounds != null) {
+                    SlippyMapBBoxChooser mapPanel = new SlippyMapBBoxChooser();
+                    mapPanel.setBoundingBox(selectedLayer.bounds);
+                    JOptionPane.showMessageDialog(null, mapPanel, tr("Show Bounds"), JOptionPane.PLAIN_MESSAGE);
+                } else {
+                    JOptionPane.showMessageDialog(null, tr("No bounding box was found for this layer."),
+                            tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+                }
+            }
+        });
+        layerManipulationButtons.add(showBoundsButton);
+
+        wmsFetchPanel.add(layerManipulationButtons, GBC.eol().insets(0,0,5,0));
+        wmsFetchPanel.add(new JLabel(tr("WMS URL")), GBC.std().insets(0,0,5,0));
+        resultingLayerField = new JTextArea(3, 40);
+        resultingLayerField.setLineWrap(true);
+        wmsFetchPanel.add(new JScrollPane(resultingLayerField, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), GBC.eop().insets(5,0,0,0).fill(GridBagConstraints.HORIZONTAL));
+
+        add(wmsFetchPanel);
+    }
+
+    private String buildRootUrl() {
+        StringBuilder a = new StringBuilder(serviceUrl.getProtocol());
+        a.append("://");
+        a.append(serviceUrl.getHost());
+        if(serviceUrl.getPort() != -1) {
+            a.append(":");
+            a.append(serviceUrl.getPort());
+        }
+        a.append(serviceUrl.getPath());
+        a.append("?");
+        if(serviceUrl.getQuery() != null) {
+            a.append(serviceUrl.getQuery());
+            if (!serviceUrl.getQuery().isEmpty() && !serviceUrl.getQuery().endsWith("&")) {
+                a.append("&");
+            }
+        }
+        return a.toString();
+    }
+
+    private String buildGetMapUrl() {
+        StringBuilder a = new StringBuilder();
+        a.append(buildRootUrl());
+        a.append("FORMAT=image/jpeg&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&Layers=");
+        a.append(commaSepLayerList());
+        a.append("&");
+
+        return a.toString();
+    }
+
+    private String commaSepLayerList() {
+        StringBuilder b = new StringBuilder();
+
+        Iterator<LayerDetails> iterator = selectedLayers.iterator();
+        while (iterator.hasNext()) {
+            LayerDetails layerDetails = iterator.next();
+            b.append(layerDetails.ident);
+            if(iterator.hasNext()) {
+                b.append(",");
+            }
+        }
+
+        return b.toString();
+    }
+
+    private void showError(String incomingData, Exception e) {
+        JOptionPane.showMessageDialog(this, tr("Could not parse WMS layer list."),
+                tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+        System.err.println("Could not parse WMS layer list. Incoming data:");
+        System.err.println(incomingData);
+        e.printStackTrace();
+    }
+
+    private void attemptGetCapabilities(String serviceUrlStr) {
+        URL getCapabilitiesUrl = null;
+        try {
+            if (!serviceUrlStr.trim().contains("capabilities")) {
+                // If the url doesn't already have GetCapabilities, add it in
+                getCapabilitiesUrl = new URL(serviceUrlStr + "VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities");
+            } else {
+                // Otherwise assume it's a good URL and let the subsequent error
+                // handling systems deal with problems
+                getCapabilitiesUrl = new URL(serviceUrlStr);
+            }
+            serviceUrl = new URL(serviceUrlStr);
+        } catch (HeadlessException e) {
+            return;
+        } catch (MalformedURLException e) {
+            JOptionPane.showMessageDialog(this, tr("Invalid service URL."),
+                    tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        String incomingData;
+        try {
+            URLConnection openConnection = getCapabilitiesUrl.openConnection();
+            InputStream inputStream = openConnection.getInputStream();
+            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
+            String line;
+            StringBuilder ba = new StringBuilder();
+            while((line = br.readLine()) != null) {
+                ba.append(line);
+                ba.append("\n");
+            }
+            incomingData = ba.toString();
+        } catch (IOException e) {
+            JOptionPane.showMessageDialog(this, tr("Could not retrieve WMS layer list."),
+                    tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        Document document;
+        try {
+            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+            builderFactory.setValidating(false);
+            builderFactory.setNamespaceAware(true);
+            DocumentBuilder builder = builderFactory.newDocumentBuilder();
+            builder.setEntityResolver(new EntityResolver() {
+                @Override
+                public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
+                    System.out.println("Ignoring DTD " + publicId + ", " + systemId);
+                    return new InputSource(new StringReader(""));
+                }
+            });
+            document = builder.parse(new InputSource(new StringReader(incomingData)));
+        } catch (ParserConfigurationException e) {
+            showError(incomingData, e);
+            return;
+        } catch (SAXException e) {
+            showError(incomingData, e);
+            return;
+        } catch (IOException e) {
+            showError(incomingData, e);
+            return;
+        }
+
+        // Some WMS service URLs specify a different base URL for their GetMap service
+        Element child = getChild(document.getDocumentElement(), "Capability");
+        child = getChild(child, "Request");
+        child = getChild(child, "GetMap");
+        child = getChild(child, "DCPType");
+        child = getChild(child, "HTTP");
+        child = getChild(child, "Get");
+        child = getChild(child, "OnlineResource");
+        if (child != null) {
+            String baseURL = child.getAttribute("xlink:href");
+            if(baseURL != null) {
+                try {
+                    System.out.println("GetCapabilities specifies a different service URL: " + baseURL);
+                    serviceUrl = new URL(baseURL);
+                } catch (MalformedURLException e1) {
+                }
+            }
+        }
+
+        try {
+            treeRootNode.setUserObject(getCapabilitiesUrl.getHost());
+            Element capabilityElem = getChild(document.getDocumentElement(), "Capability");
+            List<Element> children = getChildren(capabilityElem, "Layer");
+            List<LayerDetails> layers = parseLayers(children, new HashSet<String>());
+            updateTreeList(layers);
+        } catch(Exception e) {
+            showError(incomingData, e);
+            return;
+        }
+    }
+
+    private void updateTreeList(List<LayerDetails> layers) {
+        addLayersToTreeData(treeRootNode, layers);
+        layerTree.expandRow(0);
+    }
+
+    private void addLayersToTreeData(MutableTreeNode parent, List<LayerDetails> layers) {
+        for (LayerDetails layerDetails : layers) {
+            DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(layerDetails);
+            addLayersToTreeData(treeNode, layerDetails.children);
+            treeData.insertNodeInto(treeNode, parent, 0);
+        }
+    }
+
+    private List<LayerDetails> parseLayers(List<Element> children, Set<String> parentCrs) {
+        List<LayerDetails> details = new LinkedList<LayerDetails>();
+        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);
+
+        // The set of supported CRS/SRS for this layer
+        Set<String> crsList = new HashSet<String>();
+        // ...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
+        List<Element> crsChildren = getChildren(element, "CRS");
+        crsChildren.addAll(getChildren(element, "SRS"));
+        for (Element child : crsChildren) {
+            String crs = (String) getContent(child);
+            if(crs != null) {
+                String upperCase = crs.trim().toUpperCase();
+                crsList.add(upperCase);
+            }
+        }
+
+        // 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 = Double.parseDouble(bboxElem.getAttribute("minx"));
+                double top = Double.parseDouble(bboxElem.getAttribute("maxy"));
+                double right = Double.parseDouble(bboxElem.getAttribute("maxx"));
+                double bot = Double.parseDouble(bboxElem.getAttribute("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, crsList, josmSupportsThisLayer, bounds, childLayers);
+    }
+
+    private boolean isProjSupported(String crs) {
+        for (Projection proj : Projection.allProjections) {
+            if (proj instanceof ProjectionSubPrefs) {
+                if (((ProjectionSubPrefs) proj).getPreferencesFromCode(crs) == null) {
+                    return true;
+                }
+            } else {
+                if (proj.toCode().equals(crs)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public String getUrlName() {
+        return menuName.getText();
+    }
+
+    public String getUrl() {
+        return resultingLayerField.getText();
+    }
+
+    public static void main(String[] args) {
+        JFrame f = new JFrame("Test");
+        f.setContentPane(new AddWMSLayerPanel());
+        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        f.pack();
+        f.setVisible(true);
+    }
+
+    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 = (String) getContent(child);
+            return (content != null) ? content : empty;
+        }
+    }
+
+    private static Object getContent(Element element) {
+        NodeList nl = element.getChildNodes();
+        StringBuffer content = new StringBuffer();
+        for (int i = 0; i < nl.getLength(); i++) {
+            Node node = nl.item(i);
+            switch (node.getNodeType()) {
+            case Node.ELEMENT_NODE:
+                return node;
+            case Node.CDATA_SECTION_NODE:
+            case Node.TEXT_NODE:
+                content.append(node.getNodeValue());
+                break;
+            }
+        }
+        return content.toString().trim();
+    }
+
+    private static List<Element> getChildren(Element parent, String name) {
+        List<Element> retVal = new LinkedList<Element>();
+        for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
+            if (child instanceof Element && name.equals(child.getNodeName())) {
+                retVal.add((Element) child);
+            }
+        }
+        return retVal;
+    }
+
+    private static Element getChild(Element parent, String name) {
+        if (parent == null) {
+            return null;
+        }
+        for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
+            if (child instanceof Element && name.equals(child.getNodeName())) {
+                return (Element) child;
+            }
+        }
+        return null;
+    }
+
+    class LayerDetails {
+
+        private String name;
+        private String ident;
+        private List<LayerDetails> children;
+        private Bounds bounds;
+        private boolean supported;
+
+        public LayerDetails(String name, String ident, Set<String> crsList,
+                boolean supportedLayer, Bounds bounds,
+                List<LayerDetails> childLayers) {
+            this.name = name;
+            this.ident = ident;
+            this.supported = supportedLayer;
+            this.children = childLayers;
+            this.bounds = bounds;
+        }
+
+        public boolean isSupported() {
+            return this.supported;
+        }
+
+        @Override
+        public String toString() {
+            if(this.name == null || this.name.isEmpty()) {
+                return this.ident;
+            } else {
+                return this.name;
+            }
+        }
+
+    }
+
+    class LayerTreeCellRenderer extends DefaultTreeCellRenderer {
+        @Override
+        public Component getTreeCellRendererComponent(JTree tree, Object value,
+                boolean sel, boolean expanded, boolean leaf, int row,
+                boolean hasFocus) {
+            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf,
+                    row, hasFocus);
+            DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) value;
+            Object userObject = treeNode.getUserObject();
+            if (userObject instanceof LayerDetails) {
+                LayerDetails layer = (LayerDetails) userObject;
+                setEnabled(layer.isSupported());
+            }
+            return this;
+        }
+    }
+
+}
Index: /trunk/src/org/openstreetmap/josm/gui/preferences/ImageryPreference.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/preferences/ImageryPreference.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/gui/preferences/ImageryPreference.java	(revision 3715)
@@ -0,0 +1,796 @@
+package org.openstreetmap.josm.gui.preferences;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trc;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSlider;
+import javax.swing.JSpinner;
+import javax.swing.JTabbedPane;
+import javax.swing.JTable;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableColumnModel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
+import org.openstreetmap.josm.data.imagery.OffsetBookmark;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.gui.layer.TMSLayer;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
+import org.openstreetmap.josm.io.imagery.HTMLGrabber;
+import org.openstreetmap.josm.tools.ColorHelper;
+import org.openstreetmap.josm.tools.GBC;
+
+public class ImageryPreference implements PreferenceSetting {
+    public static class Factory implements PreferenceSettingFactory {
+        @Override
+        public PreferenceSetting createPreferenceSetting() {
+            return new ImageryPreference();
+        }
+    }
+    ImageryProvidersPanel imageryProviders;
+
+    // Common settings
+    private Color colFadeColor;
+    private JButton btnFadeColor;
+    private JSlider fadeAmount = new JSlider(0, 100);
+    private JComboBox sharpen;
+
+    // WMS Settings
+    private JComboBox browser;
+    JCheckBox overlapCheckBox;
+    JSpinner spinEast;
+    JSpinner spinNorth;
+    JSpinner spinSimConn;
+
+    //TMS settings controls
+    private JCheckBox autozoomActive = new JCheckBox();
+    private JCheckBox autoloadTiles = new JCheckBox();
+    private JSpinner minZoomLvl;
+    private JSpinner maxZoomLvl;
+    private JCheckBox addToSlippyMapChosser = new JCheckBox();
+
+    private JPanel buildCommonSettingsPanel(final PreferenceTabbedPane gui) {
+        final JPanel p = new JPanel(new GridBagLayout());
+
+        this.colFadeColor = ImageryLayer.getFadeColor();
+        this.btnFadeColor = new JButton();
+        this.btnFadeColor.setBackground(colFadeColor);
+        this.btnFadeColor.setText(ColorHelper.color2html(colFadeColor));
+
+        this.btnFadeColor.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                JColorChooser chooser = new JColorChooser(colFadeColor);
+                int answer = JOptionPane.showConfirmDialog(
+                        gui, chooser,
+                        tr("Choose a color for {0}", tr("imagery fade")),
+                        JOptionPane.OK_CANCEL_OPTION,
+                        JOptionPane.PLAIN_MESSAGE);
+                if (answer == JOptionPane.OK_OPTION) {
+                    colFadeColor = chooser.getColor();
+                    btnFadeColor.setBackground(colFadeColor);
+                    btnFadeColor.setText(ColorHelper.color2html(colFadeColor));
+                }
+            }
+        });
+
+        p.add(new JLabel(tr("Fade Color: ")), GBC.std());
+        p.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        p.add(this.btnFadeColor, GBC.eol().fill(GBC.HORIZONTAL));
+
+        p.add(new JLabel(tr("Fade amount: ")), GBC.std());
+        p.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        p.add(this.fadeAmount, GBC.eol().fill(GBC.HORIZONTAL));
+        this.fadeAmount.setValue(ImageryLayer.PROP_FADE_AMOUNT.get());
+
+        this.sharpen = new JComboBox(new String[] {
+                tr("None"),
+                tr("Soft"),
+                tr("Strong")});
+        p.add(new JLabel(tr("Sharpen (requires layer re-add): ")));
+        p.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        p.add(this.sharpen, GBC.std().fill(GBC.HORIZONTAL));
+        this.sharpen.setSelectedIndex(ImageryLayer.PROP_SHARPEN_LEVEL.get());
+
+        return p;
+    }
+
+    private JPanel buildWMSSettingsPanel() {
+        final JPanel p = new JPanel(new GridBagLayout());
+        browser = new JComboBox(new String[] {
+                "webkit-image {0}",
+                "gnome-web-photo --mode=photo --format=png {0} /dev/stdout",
+                "gnome-web-photo-fixed {0}",
+        "webkit-image-gtk {0}"});
+        browser.setEditable(true);
+        browser.setSelectedItem(HTMLGrabber.PROP_BROWSER.get());
+        p.add(new JLabel(tr("Downloader:")), GBC.eol().fill(GBC.HORIZONTAL));
+        p.add(browser);
+
+        // Overlap
+        p.add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL));
+
+        overlapCheckBox = new JCheckBox(tr("Overlap tiles"), WMSLayer.PROP_OVERLAP.get());
+        JLabel labelEast = new JLabel(tr("% of east:"));
+        JLabel labelNorth = new JLabel(tr("% of north:"));
+        spinEast = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_EAST.get(), 1, 50, 1));
+        spinNorth = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_NORTH.get(), 1, 50, 1));
+
+        JPanel overlapPanel = new JPanel(new FlowLayout());
+        overlapPanel.add(overlapCheckBox);
+        overlapPanel.add(labelEast);
+        overlapPanel.add(spinEast);
+        overlapPanel.add(labelNorth);
+        overlapPanel.add(spinNorth);
+
+        p.add(overlapPanel);
+
+        // Simultaneous connections
+        p.add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL));
+        JLabel labelSimConn = new JLabel(tr("Simultaneous connections"));
+        spinSimConn = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.get(), 1, 30, 1));
+        JPanel overlapPanelSimConn = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        overlapPanelSimConn.add(labelSimConn);
+        overlapPanelSimConn.add(spinSimConn);
+        p.add(overlapPanelSimConn, GBC.eol().fill(GBC.HORIZONTAL));
+
+        return p;
+    }
+
+    private JPanel buildTMSSettingsPanel() {
+        JPanel tmsTab = new JPanel(new GridBagLayout());
+
+        minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MIN_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
+        maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MAX_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
+
+        tmsTab.add(new JLabel(tr("Auto zoom by default: ")), GBC.std());
+        tmsTab.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        tmsTab.add(autozoomActive, GBC.eol().fill(GBC.HORIZONTAL));
+
+        tmsTab.add(new JLabel(tr("Autoload tiles by default: ")), GBC.std());
+        tmsTab.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        tmsTab.add(autoloadTiles, GBC.eol().fill(GBC.HORIZONTAL));
+
+        tmsTab.add(new JLabel(tr("Min zoom lvl: ")), GBC.std());
+        tmsTab.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        tmsTab.add(this.minZoomLvl, GBC.eol().fill(GBC.HORIZONTAL));
+
+        tmsTab.add(new JLabel(tr("Max zoom lvl: ")), GBC.std());
+        tmsTab.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        tmsTab.add(this.maxZoomLvl, GBC.eol().fill(GBC.HORIZONTAL));
+
+        tmsTab.add(new JLabel(tr("Add to slippymap chooser: ")), GBC.std());
+        tmsTab.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        tmsTab.add(addToSlippyMapChosser, GBC.eol().fill(GBC.HORIZONTAL));
+
+        this.autozoomActive.setSelected(TMSLayer.PROP_DEFAULT_AUTOZOOM.get());
+        this.autoloadTiles.setSelected(TMSLayer.PROP_DEFAULT_AUTOLOAD.get());
+        this.addToSlippyMapChosser.setSelected(TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get());
+        this.maxZoomLvl.setValue(TMSLayer.getMaxZoomLvl(null));
+        this.minZoomLvl.setValue(TMSLayer.getMinZoomLvl(null));
+        return tmsTab;
+    }
+
+    private void addSettingsSection(final JPanel p, String name, JPanel section) {
+        final JLabel lbl = new JLabel(name);
+        lbl.setFont(lbl.getFont().deriveFont(Font.BOLD));
+        p.add(lbl,GBC.std());
+        p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 0));
+        p.add(section,GBC.eol().insets(20,5,0,10));
+    }
+
+    private Component buildSettingsPanel(final PreferenceTabbedPane gui) {
+        final JPanel p = new JPanel(new GridBagLayout());
+        p.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+
+        addSettingsSection(p, tr("Common Settings"), buildCommonSettingsPanel(gui));
+        addSettingsSection(p, tr("WMS Settings"), buildWMSSettingsPanel());
+        addSettingsSection(p, tr("TMS Settings"), buildTMSSettingsPanel());
+
+        p.add(new JPanel(),GBC.eol().fill(GBC.BOTH));
+        return new JScrollPane(p);
+    }
+
+    @Override
+    public void addGui(final PreferenceTabbedPane gui) {
+        JPanel p = gui.createPreferenceTab("imagery", tr("Imagery Preferences"), tr("Modify list of imagery layers displayed in the Imagery menu"));
+        JTabbedPane pane = new JTabbedPane();
+        imageryProviders = new ImageryProvidersPanel(gui, ImageryLayerInfo.instance);
+        pane.add(imageryProviders);
+        pane.add(buildSettingsPanel(gui));
+        pane.add(new OffsetBookmarksPanel(gui));
+        pane.setTitleAt(0, tr("Imagery providers"));
+        pane.setTitleAt(1, tr("Settings"));
+        pane.setTitleAt(2, tr("Offset bookmarks"));
+        p.add(pane,GBC.std().fill(GBC.BOTH));
+    }
+
+    @Override
+    public boolean ok() {
+        boolean restartRequired = false;
+        ImageryLayerInfo.instance.save();
+        Main.main.menu.imageryMenuUpdater.refreshImageryMenu();
+        Main.main.menu.imageryMenuUpdater.refreshOffsetMenu();
+        OffsetBookmark.saveBookmarks();
+
+        WMSLayer.PROP_OVERLAP.put(overlapCheckBox.getModel().isSelected());
+        WMSLayer.PROP_OVERLAP_EAST.put((Integer) spinEast.getModel().getValue());
+        WMSLayer.PROP_OVERLAP_NORTH.put((Integer) spinNorth.getModel().getValue());
+        WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.put((Integer) spinSimConn.getModel().getValue());
+
+        HTMLGrabber.PROP_BROWSER.put(browser.getEditor().getItem().toString());
+
+        if (TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get() != this.addToSlippyMapChosser.isSelected()) {
+            restartRequired = true;
+        }
+        TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.put(this.addToSlippyMapChosser.isSelected());
+        TMSLayer.PROP_DEFAULT_AUTOZOOM.put(this.autozoomActive.isSelected());
+        TMSLayer.PROP_DEFAULT_AUTOLOAD.put(this.autoloadTiles.isSelected());
+        TMSLayer.setMaxZoomLvl((Integer)this.maxZoomLvl.getValue());
+        TMSLayer.setMinZoomLvl((Integer)this.minZoomLvl.getValue());
+
+        ImageryLayer.PROP_FADE_AMOUNT.put(this.fadeAmount.getValue());
+        ImageryLayer.setFadeColor(this.colFadeColor);
+        ImageryLayer.PROP_SHARPEN_LEVEL.put(sharpen.getSelectedIndex());
+
+        return restartRequired;
+    }
+
+
+    /**
+     * Updates a server URL in the preferences dialog. Used by plugins.
+     *
+     * @param server
+     *            The server name
+     * @param url
+     *            The server URL
+     */
+    public void setServerUrl(String server, String url) {
+        for (int i = 0; i < imageryProviders.model.getRowCount(); i++) {
+            if (server.equals(imageryProviders.model.getValueAt(i, 0).toString())) {
+                imageryProviders.model.setValueAt(url, i, 1);
+                return;
+            }
+        }
+        imageryProviders.model.addRow(new String[] { server, url });
+    }
+
+    /**
+     * Gets a server URL in the preferences dialog. Used by plugins.
+     *
+     * @param server
+     *            The server name
+     * @return The server URL
+     */
+    public String getServerUrl(String server) {
+        for (int i = 0; i < imageryProviders.model.getRowCount(); i++) {
+            if (server.equals(imageryProviders.model.getValueAt(i, 0).toString()))
+                return imageryProviders.model.getValueAt(i, 1).toString();
+        }
+        return null;
+    }
+
+    static class ImageryProvidersPanel extends JPanel {
+        final ImageryLayerTableModel model;
+        private final ImageryLayerInfo layerInfo;
+
+        public ImageryProvidersPanel(final PreferenceTabbedPane gui, ImageryLayerInfo layerInfo) {
+            super(new GridBagLayout());
+            this.layerInfo = layerInfo;
+            this.model = new ImageryLayerTableModel();
+
+            final JTable list = new JTable(model) {
+                @Override
+                public String getToolTipText(MouseEvent e) {
+                    java.awt.Point p = e.getPoint();
+                    return model.getValueAt(rowAtPoint(p), columnAtPoint(p)).toString();
+                }
+            };
+            JScrollPane scroll = new JScrollPane(list);
+            add(scroll, GBC.eol().fill(GridBagConstraints.BOTH));
+            scroll.setPreferredSize(new Dimension(200, 200));
+
+            final ImageryDefaultLayerTableModel modeldef = new ImageryDefaultLayerTableModel();
+            final JTable listdef = new JTable(modeldef) {
+                @Override
+                public String getToolTipText(MouseEvent e) {
+                    java.awt.Point p = e.getPoint();
+                    return (String) modeldef.getValueAt(rowAtPoint(p), columnAtPoint(p));
+                }
+            };
+            JScrollPane scrolldef = new JScrollPane(listdef);
+            // scrolldef is added after the buttons so it's clearer the buttons
+            // control the top list and not the default one
+            scrolldef.setPreferredSize(new Dimension(200, 200));
+
+            TableColumnModel mod = listdef.getColumnModel();
+            mod.getColumn(1).setPreferredWidth(800);
+            mod.getColumn(0).setPreferredWidth(200);
+            mod = list.getColumnModel();
+            mod.getColumn(2).setPreferredWidth(50);
+            mod.getColumn(1).setPreferredWidth(800);
+            mod.getColumn(0).setPreferredWidth(200);
+
+            JPanel buttonPanel = new JPanel(new FlowLayout());
+
+            JButton add = new JButton(tr("Add"));
+            buttonPanel.add(add, GBC.std().insets(0, 5, 0, 0));
+            add.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    AddWMSLayerPanel p = new AddWMSLayerPanel();
+                    int answer = JOptionPane.showConfirmDialog(
+                            gui, p,
+                            tr("Add Imagery URL"),
+                            JOptionPane.OK_CANCEL_OPTION);
+                    if (answer == JOptionPane.OK_OPTION) {
+                        model.addRow(new ImageryInfo(p.getUrlName(), p.getUrl()));
+                    }
+                }
+            });
+
+            JButton delete = new JButton(tr("Delete"));
+            buttonPanel.add(delete, GBC.std().insets(0, 5, 0, 0));
+            delete.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    if (list.getSelectedRow() == -1) {
+                        JOptionPane.showMessageDialog(gui, tr("Please select the row to delete."));
+                    } else {
+                        Integer i;
+                        while ((i = list.getSelectedRow()) != -1) {
+                            model.removeRow(i);
+                        }
+                    }
+                }
+            });
+
+            JButton copy = new JButton(tr("Copy Selected Default(s)"));
+            buttonPanel.add(copy, GBC.std().insets(0, 5, 0, 0));
+            copy.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    int[] lines = listdef.getSelectedRows();
+                    if (lines.length == 0) {
+                        JOptionPane.showMessageDialog(
+                                gui,
+                                tr("Please select at least one row to copy."),
+                                tr("Information"),
+                                JOptionPane.INFORMATION_MESSAGE);
+                        return;
+                    }
+
+                    outer: for (int i = 0; i < lines.length; i++) {
+                        ImageryInfo info = modeldef.getRow(lines[i]);
+
+                        // Check if an entry with exactly the same values already
+                        // exists
+                        for (int j = 0; j < model.getRowCount(); j++) {
+                            if (info.equalsBaseValues(model.getRow(j))) {
+                                // Select the already existing row so the user has
+                                // some feedback in case an entry exists
+                                list.getSelectionModel().setSelectionInterval(j, j);
+                                list.scrollRectToVisible(list.getCellRect(j, 0, true));
+                                continue outer;
+                            }
+                        }
+
+                        if (info.eulaAcceptanceRequired != null) {
+                            if (!confirmEulaAcceptance(gui, info.eulaAcceptanceRequired)) {
+                                continue outer;
+                            }
+                        }
+
+                        model.addRow(new ImageryInfo(info));
+                        int lastLine = model.getRowCount() - 1;
+                        list.getSelectionModel().setSelectionInterval(lastLine, lastLine);
+                        list.scrollRectToVisible(list.getCellRect(lastLine, 0, true));
+                    }
+                }
+            });
+
+            add(buttonPanel);
+            add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+            // Add default item list
+            add(scrolldef, GBC.eol().insets(0, 5, 0, 0).fill(GridBagConstraints.BOTH));
+        }
+
+        /**
+         * The table model for imagery layer list
+         */
+        class ImageryLayerTableModel extends DefaultTableModel {
+            public ImageryLayerTableModel() {
+                setColumnIdentifiers(new String[] { tr("Menu Name"), tr("Imagery URL"), trc("layer", "Zoom") });
+            }
+
+            public ImageryInfo getRow(int row) {
+                return layerInfo.getLayers().get(row);
+            }
+
+            public void addRow(ImageryInfo i) {
+                layerInfo.add(i);
+                int p = getRowCount() - 1;
+                fireTableRowsInserted(p, p);
+            }
+
+            @Override
+            public void removeRow(int i) {
+                layerInfo.remove(getRow(i));
+                fireTableRowsDeleted(i, i);
+            }
+
+            @Override
+            public int getRowCount() {
+                return layerInfo.getLayers().size();
+            }
+
+            @Override
+            public Object getValueAt(int row, int column) {
+                ImageryInfo info = layerInfo.getLayers().get(row);
+                switch (column) {
+                case 0:
+                    return info.getName();
+                case 1:
+                    return info.getFullURL();
+                case 2:
+                    return (info.getImageryType() == ImageryType.WMS || info.getImageryType() == ImageryType.HTML) ?
+                            (info.getPixelPerDegree() == 0.0 ? "" : info.getPixelPerDegree()) :
+                                (info.getMaxZoom() == 0 ? "" : info.getMaxZoom());
+                default:
+                    throw new ArrayIndexOutOfBoundsException();
+                }
+            }
+
+            @Override
+            public void setValueAt(Object o, int row, int column) {
+                ImageryInfo info = layerInfo.getLayers().get(row);
+                switch (column) {
+                case 0:
+                    info.setName((String) o);
+                    break;
+                case 1:
+                    info.setURL((String)o);
+                    break;
+                case 2:
+                    info.setPixelPerDegree(0);
+                    info.setMaxZoom(0);
+                    try {
+                        if(info.getImageryType() == ImageryType.WMS || info.getImageryType() == ImageryType.HTML) {
+                            info.setPixelPerDegree(Double.parseDouble((String) o));
+                        } else {
+                            info.setMaxZoom(Integer.parseInt((String) o));
+                        }
+                    } catch (NumberFormatException e) {
+                    }
+                    break;
+                default:
+                    throw new ArrayIndexOutOfBoundsException();
+                }
+            }
+
+            @Override
+            public boolean isCellEditable(int row, int column) {
+                return true;
+            }
+        }
+
+        /**
+         * The table model for the default imagery layer list
+         */
+        class ImageryDefaultLayerTableModel extends DefaultTableModel {
+            public ImageryDefaultLayerTableModel() {
+                setColumnIdentifiers(new String[] { tr("Menu Name (Default)"), tr("Imagery URL (Default)") });
+            }
+
+            public ImageryInfo getRow(int row) {
+                return layerInfo.getDefaultLayers().get(row);
+            }
+
+            @Override
+            public int getRowCount() {
+                return layerInfo.getDefaultLayers().size();
+            }
+
+            @Override
+            public Object getValueAt(int row, int column) {
+                ImageryInfo info = layerInfo.getDefaultLayers().get(row);
+                switch (column) {
+                case 0:
+                    return info.getName();
+                case 1:
+                    return info.getFullURL();
+                }
+                return null;
+            }
+
+            @Override
+            public boolean isCellEditable(int row, int column) {
+                return false;
+            }
+        }
+
+        private boolean confirmEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) {
+            URL url = null;
+            try {
+                url = new URL(eulaUrl.replaceAll("\\{lang\\}", Locale.getDefault().toString()));
+                JEditorPane htmlPane = null;
+                try {
+                    htmlPane = new JEditorPane(url);
+                } catch (IOException e1) {
+                    // give a second chance with a default Locale 'en'
+                    try {
+                        url = new URL(eulaUrl.replaceAll("\\{lang\\}", "en"));
+                        htmlPane = new JEditorPane(url);
+                    } catch (IOException e2) {
+                        JOptionPane.showMessageDialog(gui ,tr("EULA license URL not available: {0}", eulaUrl));
+                        return false;
+                    }
+                }
+                Box box = Box.createVerticalBox();
+                htmlPane.setEditable(false);
+                JScrollPane scrollPane = new JScrollPane(htmlPane);
+                scrollPane.setPreferredSize(new Dimension(400, 400));
+                box.add(scrollPane);
+                int option = JOptionPane.showConfirmDialog(Main.parent, box, tr("Please abort if you are not sure"), JOptionPane.YES_NO_OPTION,
+                        JOptionPane.WARNING_MESSAGE);
+                if (option == JOptionPane.YES_OPTION)
+                    return true;
+            } catch (MalformedURLException e2) {
+                JOptionPane.showMessageDialog(gui ,tr("Malformed URL for the EULA licence: {0}", eulaUrl));
+            }
+            return false;
+        }
+    }
+
+    static class OffsetBookmarksPanel extends JPanel {
+        List<OffsetBookmark> bookmarks = OffsetBookmark.allBookmarks;
+        OffsetsBookmarksModel model = new OffsetsBookmarksModel();
+
+        public OffsetBookmarksPanel(final PreferenceTabbedPane gui) {
+            super(new GridBagLayout());
+            final JTable list = new JTable(model) {
+                @Override
+                public String getToolTipText(MouseEvent e) {
+                    java.awt.Point p = e.getPoint();
+                    return model.getValueAt(rowAtPoint(p), columnAtPoint(p)).toString();
+                }
+            };
+            JScrollPane scroll = new JScrollPane(list);
+            add(scroll, GBC.eol().fill(GridBagConstraints.BOTH));
+            scroll.setPreferredSize(new Dimension(200, 200));
+
+            TableColumnModel mod = list.getColumnModel();
+            mod.getColumn(0).setPreferredWidth(150);
+            mod.getColumn(1).setPreferredWidth(200);
+            mod.getColumn(2).setPreferredWidth(300);
+            mod.getColumn(3).setPreferredWidth(150);
+            mod.getColumn(4).setPreferredWidth(150);
+
+            JPanel buttonPanel = new JPanel(new FlowLayout());
+
+            JButton add = new JButton(tr("Add"));
+            buttonPanel.add(add, GBC.std().insets(0, 5, 0, 0));
+            add.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    OffsetBookmark b = new OffsetBookmark(Main.proj,"","",0,0);
+                    model.addRow(b);
+                }
+            });
+
+            JButton delete = new JButton(tr("Delete"));
+            buttonPanel.add(delete, GBC.std().insets(0, 5, 0, 0));
+            delete.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    if (list.getSelectedRow() == -1) {
+                        JOptionPane.showMessageDialog(gui, tr("Please select the row to delete."));
+                    } else {
+                        Integer i;
+                        while ((i = list.getSelectedRow()) != -1) {
+                            model.removeRow(i);
+                        }
+                    }
+                }
+            });
+
+            add(buttonPanel,GBC.eol());
+        }
+
+        /**
+         * The table model for imagery offsets list
+         */
+        class OffsetsBookmarksModel extends DefaultTableModel {
+            public OffsetsBookmarksModel() {
+                setColumnIdentifiers(new String[] { tr("Projection"),  tr("Layer"), tr("Name"), tr("Easting"), tr("Northing"),});
+            }
+
+            public OffsetBookmark getRow(int row) {
+                return bookmarks.get(row);
+            }
+
+            public void addRow(OffsetBookmark i) {
+                bookmarks.add(i);
+                int p = getRowCount() - 1;
+                fireTableRowsInserted(p, p);
+            }
+
+            @Override
+            public void removeRow(int i) {
+                bookmarks.remove(getRow(i));
+                fireTableRowsDeleted(i, i);
+            }
+
+            @Override
+            public int getRowCount() {
+                return bookmarks.size();
+            }
+
+            @Override
+            public Object getValueAt(int row, int column) {
+                OffsetBookmark info = bookmarks.get(row);
+                switch (column) {
+                case 0:
+                    return info.proj.toString();
+                case 1:
+                    return info.layerName;
+                case 2:
+                    return info.name;
+                case 3:
+                    return info.dx;
+                case 4:
+                    return info.dy;
+                default:
+                    throw new ArrayIndexOutOfBoundsException();
+                }
+            }
+
+            @Override
+            public void setValueAt(Object o, int row, int column) {
+                OffsetBookmark info = bookmarks.get(row);
+                switch (column) {
+                case 1:
+                    info.layerName = o.toString();
+                    break;
+                case 2:
+                    info.name = o.toString();
+                    break;
+                case 3:
+                    info.dx = Double.parseDouble((String) o);
+                    break;
+                case 4:
+                    info.dy = Double.parseDouble((String) o);
+                    break;
+                default:
+                    throw new ArrayIndexOutOfBoundsException();
+                }
+            }
+
+            @Override
+            public boolean isCellEditable(int row, int column) {
+                return column >= 1;
+            }
+        }
+    }
+
+    public static void initialize() {
+        migrateWMSPlugin();
+        migrateSlippyMapPlugin();
+        ImageryLayerInfo.instance.load();
+        OffsetBookmark.loadBookmarks();
+        Main.main.menu.imageryMenuUpdater.refreshImageryMenu();
+        Main.main.menu.imageryMenuUpdater.refreshOffsetMenu();
+    }
+
+    // Migration of WMSPlugin and SlippyMap settings
+    static boolean wmsLayersConflict;
+    static boolean wmsSettingsConflict;
+    static boolean tmsSettingsConflict;
+
+    static class SettingsConflictException extends Exception {
+    }
+
+    static void migrateProperty(String oldProp, String newProp)
+    throws SettingsConflictException {
+        String oldValue = Main.pref.get(oldProp, null);
+        if (oldValue == null) return;
+        String newValue = Main.pref.get(newProp, null);
+        if (newValue != null && !oldValue.equals(newValue)) {
+            System.out.println(tr("Imagery settings migration: conflict when moving property {0} -> {1}",
+                    oldProp, newProp));
+            throw new SettingsConflictException();
+        }
+        Main.pref.put(newProp, oldValue);
+        Main.pref.put(oldProp, null);
+    }
+
+    static void migrateWMSPlugin() {
+        try {
+            migrateProperty("wmslayers", "imagery.layers");
+        } catch (SettingsConflictException e) {
+            wmsLayersConflict = true;
+        }
+        try {
+            Main.pref.put("wmslayers.default", null);
+            migrateProperty("imagery.remotecontrol", "remotecontrol.permission.imagery");
+            migrateProperty("wmsplugin.remotecontrol", "remotecontrol.permission.imagery");
+            migrateProperty("wmsplugin.alpha_channel", "imagery.wms.alpha_channel");
+            migrateProperty("wmsplugin.browser", "imagery.wms.browser");
+            migrateProperty("wmsplugin.user_agent", "imagery.wms.user_agent");
+            migrateProperty("wmsplugin.timeout.connect", "imagery.wms.timeout.connect");
+            migrateProperty("wmsplugin.timeout.read", "imagery.wms.timeout.read");
+            migrateProperty("wmsplugin.simultaneousConnections", "imagery.wms.simultaneousConnections");
+            migrateProperty("wmsplugin.overlap", "imagery.wms.overlap");
+            migrateProperty("wmsplugin.overlapEast", "imagery.wms.overlapEast");
+            migrateProperty("wmsplugin.overlapNorth", "imagery.wms.overlapNorth");
+            Map<String, String> unknownProps = Main.pref.getAllPrefix("wmsplugin");
+            if (!unknownProps.isEmpty()) {
+                System.out.println(tr("There are {0} unknown WMSPlugin settings", unknownProps.size()));
+                wmsSettingsConflict = true;
+            }
+        } catch (SettingsConflictException e) {
+            wmsSettingsConflict = true;
+        }
+    }
+
+    static void migrateSlippyMapPlugin() {
+        try {
+            Main.pref.put("slippymap.tile_source", null);
+            Main.pref.put("slippymap.last_zoom_lvl", null);
+            migrateProperty("slippymap.draw_debug", "imagery.tms.draw_debug");
+            migrateProperty("slippymap.autoload_tiles", "imagery.tms.autoload");
+            migrateProperty("slippymap.autozoom", "imagery.tms.autozoom");
+            migrateProperty("slippymap.min_zoom_lvl", "imagery.tms.min_zoom_lvl");
+            migrateProperty("slippymap.max_zoom_lvl", "imagery.tms.max_zoom_lvl");
+            if (Main.pref.get("slippymap.fade_background_100", null) == null) {
+                try {
+                    Main.pref.putInteger("slippymap.fade_background_100", (int)Math.round(
+                            Double.valueOf(Main.pref.get("slippymap.fade_background", "0"))*100.0));
+                } catch (NumberFormatException e) {
+                }
+            }
+            Main.pref.put("slippymap.fade_background", null);
+            migrateProperty("slippymap.fade_background_100", "imagery.fade_amount");
+            Map<String, String> unknownProps = Main.pref.getAllPrefix("slippymap");
+            if (!unknownProps.isEmpty()) {
+                System.out.println(tr("There are {0} unknown slippymap plugin settings", unknownProps.size()));
+                wmsSettingsConflict = true;
+            }
+        } catch (SettingsConflictException e) {
+            tmsSettingsConflict = true;
+        }
+    }
+
+}
Index: /trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java	(revision 3715)
@@ -26,5 +26,4 @@
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
 import org.openstreetmap.josm.plugins.PluginDownloadTask;
 import org.openstreetmap.josm.plugins.PluginHandler;
@@ -45,5 +44,5 @@
 
     /**
-     * Allows PreferenceSettings to do validation of entered values when ok was pressed. 
+     * Allows PreferenceSettings to do validation of entered values when ok was pressed.
      * If data is invalid then event can return false to cancel closing of preferences dialog.
      *
@@ -278,7 +277,6 @@
         settingsFactory.add(new ShortcutPreference.Factory());
         settingsFactory.add(new ValidatorPreference.Factory());
-        if (RemoteControl.on) {
-            settingsFactory.add(new RemoteControlPreference.Factory());
-        }
+        settingsFactory.add(new RemoteControlPreference.Factory());
+        settingsFactory.add(new ImageryPreference.Factory());
 
         PluginHandler.getPreferenceSetting(settingsFactory);
Index: /trunk/src/org/openstreetmap/josm/gui/preferences/RemoteControlPreference.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/preferences/RemoteControlPreference.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/gui/preferences/RemoteControlPreference.java	(revision 3715)
@@ -7,7 +7,7 @@
 import java.awt.Font;
 import java.awt.GridBagLayout;
-
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+
 import javax.swing.BorderFactory;
 import javax.swing.Box;
@@ -15,11 +15,12 @@
 import javax.swing.JLabel;
 import javax.swing.JPanel;
-
 import javax.swing.JSeparator;
 import javax.swing.UIManager;
+
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
 import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler;
+import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler;
 import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler;
 import org.openstreetmap.josm.io.remotecontrol.handler.LoadAndZoomHandler;
@@ -46,4 +47,5 @@
     private JCheckBox permissionLoadData = new JCheckBox(tr("load data from API"));
     private JCheckBox permissionImportData = new JCheckBox(tr("import data from URL"));
+    private JCheckBox permissionLoadImagery = new JCheckBox(tr("load imagery layers"));
     private JCheckBox permissionCreateObjects = new JCheckBox(tr("create new objects"));
     private JCheckBox permissionChangeSelection = new JCheckBox(tr("change the selection"));
@@ -75,4 +77,5 @@
         wrapper.add(permissionLoadData, GBC.eol().insets(INDENT,5,0,0).fill(GBC.HORIZONTAL));
         wrapper.add(permissionImportData, GBC.eol().insets(INDENT,5,0,0).fill(GBC.HORIZONTAL));
+        wrapper.add(permissionLoadImagery, GBC.eol().insets(INDENT,5,0,0).fill(GBC.HORIZONTAL));
         wrapper.add(permissionChangeSelection, GBC.eol().insets(INDENT,5,0,0).fill(GBC.HORIZONTAL));
         wrapper.add(permissionChangeViewport, GBC.eol().insets(INDENT,5,0,0).fill(GBC.HORIZONTAL));
@@ -94,4 +97,5 @@
         permissionLoadData.setSelected(Main.pref.getBoolean(LoadAndZoomHandler.loadDataPermissionKey, LoadAndZoomHandler.loadDataPermissionDefault));
         permissionImportData.setSelected(Main.pref.getBoolean(ImportHandler.permissionKey, ImportHandler.permissionDefault));
+        permissionLoadImagery.setSelected(Main.pref.getBoolean(ImageryHandler.permissionKey, ImageryHandler.permissionDefault));
         permissionChangeSelection.setSelected(Main.pref.getBoolean(LoadAndZoomHandler.changeSelectionPermissionKey, LoadAndZoomHandler.changeSelectionPermissionDefault));
         permissionChangeViewport.setSelected(Main.pref.getBoolean(LoadAndZoomHandler.changeViewportPermissionKey, LoadAndZoomHandler.changeViewportPermissionDefault));
@@ -120,4 +124,5 @@
             Main.pref.put(LoadAndZoomHandler.loadDataPermissionKey, permissionLoadData.isSelected());
             Main.pref.put(ImportHandler.permissionKey, permissionImportData.isSelected());
+            Main.pref.put(ImageryHandler.permissionKey, permissionLoadImagery.isSelected());
             Main.pref.put(LoadAndZoomHandler.changeSelectionPermissionKey, permissionChangeSelection.isSelected());
             Main.pref.put(LoadAndZoomHandler.changeViewportPermissionKey, permissionChangeViewport.isSelected());
Index: /trunk/src/org/openstreetmap/josm/io/WMSLayerExporter.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/WMSLayerExporter.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/io/WMSLayerExporter.java	(revision 3715)
@@ -0,0 +1,12 @@
+package org.openstreetmap.josm.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+
+public class WMSLayerExporter extends FileExporter{
+
+    public WMSLayerExporter() {
+        super(new ExtensionFileFilter("wms", "wms", tr("WMS Files (*.wms)")));
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/io/WMSLayerImporter.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/WMSLayerImporter.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/io/WMSLayerImporter.java	(revision 3715)
@@ -0,0 +1,13 @@
+package org.openstreetmap.josm.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+
+public class WMSLayerImporter extends FileImporter{
+
+    public WMSLayerImporter() {
+        super(new ExtensionFileFilter("wms", "wms", tr("WMS Files (*.wms)")));
+    }
+
+}
Index: /trunk/src/org/openstreetmap/josm/io/imagery/Grabber.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/imagery/Grabber.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/io/imagery/Grabber.java	(revision 3715)
@@ -0,0 +1,112 @@
+package org.openstreetmap.josm.io.imagery;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.imagery.GeorefImage.State;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
+import org.openstreetmap.josm.io.CacheFiles;
+
+abstract public class Grabber implements Runnable {
+    public final static CacheFiles cache = new CacheFiles("imagery");
+
+    protected final MapView mv;
+    protected final WMSLayer layer;
+
+    protected ProjectionBounds b;
+    protected Projection proj;
+    protected double pixelPerDegree;
+    protected WMSRequest request;
+    protected volatile boolean canceled;
+
+    Grabber(MapView mv, WMSLayer layer, CacheFiles cache) {
+        this.mv = mv;
+        this.layer = layer;
+    }
+
+    private void updateState(WMSRequest request) {
+        b = new ProjectionBounds(
+                layer.getEastNorth(request.getXIndex(), request.getYIndex()),
+                layer.getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
+        if (b.min != null && b.max != null && WMSLayer.PROP_OVERLAP.get()) {
+            double eastSize =  b.max.east() - b.min.east();
+            double northSize =  b.max.north() - b.min.north();
+
+            double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
+            double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
+
+            this.b = new ProjectionBounds( new EastNorth(b.min.east(),
+                    b.min.north()),
+                    new EastNorth(b.max.east() + eastCoef * eastSize,
+                            b.max.north() + northCoef * northSize));
+        }
+
+        this.proj = Main.proj;
+        this.pixelPerDegree = request.getPixelPerDegree();
+        this.request = request;
+    }
+
+    abstract void fetch(WMSRequest request) throws Exception; // the image fetch code
+
+    int width(){
+        return layer.getBaseImageWidth();
+    }
+    int height(){
+        return layer.getBaseImageHeight();
+    }
+
+    @Override
+    public void run() {
+        while (true) {
+            if (canceled)
+                return;
+            WMSRequest request = layer.getRequest();
+            if (request == null)
+                return;
+            updateState(request);
+            if(!loadFromCache(request)){
+                attempt(request);
+            }
+            if (request.getState() != null) {
+                layer.finishRequest(request);
+                mv.repaint();
+            }
+        }
+    }
+
+    protected void attempt(WMSRequest request){ // try to fetch the image
+        int maxTries = 5; // n tries for every image
+        for (int i = 1; i <= maxTries; i++) {
+            if (canceled)
+                return;
+            try {
+                if (!layer.requestIsValid(request))
+                    return;
+                fetch(request);
+                break; // break out of the retry loop
+            } catch (Exception e) {
+                try { // sleep some time and then ask the server again
+                    Thread.sleep(random(1000, 2000));
+                } catch (InterruptedException e1) {}
+
+                if(i == maxTries) {
+                    e.printStackTrace();
+                    request.finish(State.FAILED, null);
+                }
+            }
+        }
+    }
+
+    public static int random(int min, int max) {
+        return (int)(Math.random() * ((max+1)-min) ) + min;
+    }
+
+    abstract public boolean loadFromCache(WMSRequest request);
+
+    public void cancel() {
+        canceled = true;
+    }
+
+}
Index: /trunk/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java	(revision 3715)
@@ -0,0 +1,49 @@
+package org.openstreetmap.josm.io.imagery;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.net.URL;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+
+import javax.imageio.ImageIO;
+
+import org.openstreetmap.josm.data.preferences.StringProperty;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
+import org.openstreetmap.josm.io.CacheFiles;
+
+public class HTMLGrabber extends WMSGrabber {
+    public static final StringProperty PROP_BROWSER = new StringProperty("imagery.wms.browser", "webkit-image {0}");
+
+    public HTMLGrabber(MapView mv, WMSLayer layer, CacheFiles cache) {
+        super(mv, layer, cache);
+    }
+
+    @Override
+    protected BufferedImage grab(URL url) throws IOException {
+        String urlstring = url.toExternalForm();
+
+        System.out.println("Grabbing HTML " + url);
+
+        ArrayList<String> cmdParams = new ArrayList<String>();
+        StringTokenizer st = new StringTokenizer(MessageFormat.format(PROP_BROWSER.get(), urlstring));
+        while( st.hasMoreTokens() ) {
+            cmdParams.add(st.nextToken());
+        }
+
+        ProcessBuilder builder = new ProcessBuilder( cmdParams);
+
+        Process browser;
+        try {
+            browser = builder.start();
+        } catch(IOException ioe) {
+            throw new IOException( "Could not start browser. Please check that the executable path is correct.\n" + ioe.getMessage() );
+        }
+
+        BufferedImage img = ImageIO.read(browser.getInputStream());
+        cache.saveImg(urlstring, img);
+        return img;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java	(revision 3715)
@@ -0,0 +1,203 @@
+package org.openstreetmap.josm.io.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.imageio.ImageIO;
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.GeorefImage.State;
+import org.openstreetmap.josm.data.projection.Mercator;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.WMSLayer;
+import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.openstreetmap.josm.io.ProgressInputStream;
+
+
+public class WMSGrabber extends Grabber {
+
+    protected String baseURL;
+    private final boolean urlWithPatterns;
+
+    public WMSGrabber(MapView mv, WMSLayer layer, CacheFiles cache) {
+        super(mv, layer, cache);
+        this.baseURL = layer.getInfo().getURL();
+        /* URL containing placeholders? */
+        urlWithPatterns = ImageryInfo.isUrlWithPatterns(baseURL);
+    }
+
+    @Override
+    void fetch(WMSRequest request) throws Exception{
+        URL url = null;
+        try {
+            url = getURL(
+                    b.min.east(), b.min.north(),
+                    b.max.east(), b.max.north(),
+                    width(), height());
+            request.finish(State.IMAGE, grab(url));
+
+        } catch(Exception e) {
+            e.printStackTrace();
+            throw new Exception(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""));
+        }
+    }
+
+    public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000",
+            new DecimalFormatSymbols(Locale.US));
+
+    protected URL getURL(double w, double s,double e,double n,
+            int wi, int ht) throws MalformedURLException {
+        String myProj = Main.proj.toCode();
+        if(Main.proj instanceof Mercator) // don't use mercator code directly
+        {
+            LatLon sw = Main.proj.eastNorth2latlon(new EastNorth(w, s));
+            LatLon ne = Main.proj.eastNorth2latlon(new EastNorth(e, n));
+            myProj = "EPSG:4326";
+            s = sw.lat();
+            w = sw.lon();
+            n = ne.lat();
+            e = ne.lon();
+        }
+
+        String str = baseURL;
+        String bbox = latLonFormat.format(w) + ","
+        + latLonFormat.format(s) + ","
+        + latLonFormat.format(e) + ","
+        + latLonFormat.format(n);
+
+        if (urlWithPatterns) {
+            str = str.replaceAll("\\{proj\\}", myProj)
+            .replaceAll("\\{bbox\\}", bbox)
+            .replaceAll("\\{w\\}", latLonFormat.format(w))
+            .replaceAll("\\{s\\}", latLonFormat.format(s))
+            .replaceAll("\\{e\\}", latLonFormat.format(e))
+            .replaceAll("\\{n\\}", latLonFormat.format(n))
+            .replaceAll("\\{width\\}", String.valueOf(wi))
+            .replaceAll("\\{height\\}", String.valueOf(ht));
+        } else {
+            str += "bbox=" + bbox
+            + getProjection(baseURL, false)
+            + "&width=" + wi + "&height=" + ht;
+            if (!(baseURL.endsWith("&") || baseURL.endsWith("?"))) {
+                System.out.println(tr("Warning: The base URL ''{0}'' for a WMS service doesn't have a trailing '&' or a trailing '?'.", baseURL));
+                System.out.println(tr("Warning: Fetching WMS tiles is likely to fail. Please check you preference settings."));
+                System.out.println(tr("Warning: The complete URL is ''{0}''.", str));
+            }
+        }
+        return new URL(str.replace(" ", "%20"));
+    }
+
+    static public String getProjection(String baseURL, Boolean warn)
+    {
+        String projname = Main.proj.toCode();
+        if(Main.proj instanceof Mercator) {
+            projname = "EPSG:4326";
+        }
+        String res = "";
+        try
+        {
+            Matcher m = Pattern.compile(".*srs=([a-z0-9:]+).*").matcher(baseURL.toLowerCase());
+            if(m.matches())
+            {
+                projname = projname.toLowerCase();
+                if(!projname.equals(m.group(1)) && warn)
+                {
+                    JOptionPane.showMessageDialog(Main.parent,
+                            tr("The projection ''{0}'' in URL and current projection ''{1}'' mismatch.\n"
+                                    + "This may lead to wrong coordinates.",
+                                    m.group(1), projname),
+                                    tr("Warning"),
+                                    JOptionPane.WARNING_MESSAGE);
+                }
+            } else {
+                res ="&srs="+projname;
+            }
+        }
+        catch(Exception e)
+        {
+        }
+        return res;
+    }
+
+    @Override
+    public boolean loadFromCache(WMSRequest request) {
+        URL url = null;
+        try{
+            url = getURL(
+                    b.min.east(), b.min.north(),
+                    b.max.east(), b.max.north(),
+                    width(), height());
+        } catch(Exception e) {
+            return false;
+        }
+        BufferedImage cached = cache.getImg(url.toString());
+        if((!request.isReal() && !layer.hasAutoDownload()) || cached != null){
+            if(cached == null){
+                request.finish(State.NOT_IN_CACHE, null);
+                return true;
+            }
+            request.finish(State.IMAGE, cached);
+            return true;
+        }
+        return false;
+    }
+
+    protected BufferedImage grab(URL url) throws IOException, OsmTransferException {
+        System.out.println("Grabbing WMS " + url);
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        if(layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().equals("")) {
+            conn.setRequestProperty("Cookie", layer.getInfo().getCookies());
+        }
+        conn.setRequestProperty("User-Agent", Main.pref.get("imagery.wms.user_agent", Version.getInstance().getAgentString()));
+        conn.setConnectTimeout(Main.pref.getInteger("imagery.wms.timeout.connect", 30) * 1000);
+        conn.setReadTimeout(Main.pref.getInteger("imagery.wms.timeout.read", 30) * 1000);
+
+        String contentType = conn.getHeaderField("Content-Type");
+        if( conn.getResponseCode() != 200
+                || contentType != null && !contentType.startsWith("image") )
+            throw new IOException(readException(conn));
+
+        InputStream is = new ProgressInputStream(conn, null);
+        BufferedImage img = ImageIO.read(is);
+        is.close();
+
+        cache.saveImg(url.toString(), img);
+        return img;
+    }
+
+    protected String readException(URLConnection conn) throws IOException {
+        StringBuilder exception = new StringBuilder();
+        InputStream in = conn.getInputStream();
+        BufferedReader br = new BufferedReader(new InputStreamReader(in));
+
+        String line = null;
+        while( (line = br.readLine()) != null) {
+            // filter non-ASCII characters and control characters
+            exception.append(line.replaceAll("[^\\p{Print}]", ""));
+            exception.append('\n');
+        }
+        return exception.toString();
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java	(revision 3715)
@@ -0,0 +1,103 @@
+package org.openstreetmap.josm.io.imagery;
+
+import java.awt.image.BufferedImage;
+
+import org.openstreetmap.josm.data.imagery.GeorefImage;
+import org.openstreetmap.josm.data.imagery.GeorefImage.State;
+
+public class WMSRequest implements Comparable<WMSRequest> {
+    private final int xIndex;
+    private final int yIndex;
+    private final double pixelPerDegree;
+    private final boolean real; // Download even if autodownloading is disabled
+    private int priority;
+    // Result
+    private State state;
+    private BufferedImage image;
+
+    public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real) {
+        this.xIndex = xIndex;
+        this.yIndex = yIndex;
+        this.pixelPerDegree = pixelPerDegree;
+        this.real = real;
+    }
+
+    public void finish(State state, BufferedImage image) {
+        this.state = state;
+        this.image = image;
+    }
+
+    public int getXIndex() {
+        return xIndex;
+    }
+
+    public int getYIndex() {
+        return yIndex;
+    }
+
+    public double getPixelPerDegree() {
+        return pixelPerDegree;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        long temp;
+        temp = Double.doubleToLongBits(pixelPerDegree);
+        result = prime * result + (int) (temp ^ (temp >>> 32));
+        result = prime * result + xIndex;
+        result = prime * result + yIndex;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        WMSRequest other = (WMSRequest) obj;
+        if (Double.doubleToLongBits(pixelPerDegree) != Double
+                .doubleToLongBits(other.pixelPerDegree))
+            return false;
+        if (xIndex != other.xIndex)
+            return false;
+        if (yIndex != other.yIndex)
+            return false;
+        return true;
+    }
+
+    public void setPriority(int priority) {
+        this.priority = priority;
+    }
+
+    public int getPriority() {
+        return priority;
+    }
+
+    @Override
+    public int compareTo(WMSRequest o) {
+        return priority - o.priority;
+    }
+
+    public State getState() {
+        return state;
+    }
+
+    public BufferedImage getImage() {
+        return image;
+    }
+
+    @Override
+    public String toString() {
+        return "WMSRequest [xIndex=" + xIndex + ", yIndex=" + yIndex
+        + ", pixelPerDegree=" + pixelPerDegree + "]";
+    }
+
+    public boolean isReal() {
+        return real;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/io/remotecontrol/RemoteControl.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/remotecontrol/RemoteControl.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/io/remotecontrol/RemoteControl.java	(revision 3715)
@@ -15,7 +15,4 @@
 public class RemoteControl
 {
-    // deactivate the remote control code for now. FIXME: Remove this completely if it gets turned on.
-    static final public boolean on = false;
-
     /**
      * If the remote cotrol feature is enabled or disabled. If disabled,
Index: /trunk/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java	(revision 3715)
@@ -16,4 +16,5 @@
 
 import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler;
+import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler;
 import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler;
 import org.openstreetmap.josm.io.remotecontrol.handler.LoadAndZoomHandler;
@@ -112,4 +113,5 @@
         addRequestHandlerClass(LoadAndZoomHandler.command2,
                 LoadAndZoomHandler.class, true);
+        addRequestHandlerClass(ImageryHandler.command, ImageryHandler.class, true);
         addRequestHandlerClass(AddNodeHandler.command, AddNodeHandler.class, true);
         addRequestHandlerClass(ImportHandler.command, ImportHandler.class, true);
Index: /trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/ImageryHandler.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/ImageryHandler.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/ImageryHandler.java	(revision 3715)
@@ -0,0 +1,113 @@
+package org.openstreetmap.josm.io.remotecontrol.handler;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.StringTokenizer;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+
+public class ImageryHandler extends RequestHandler {
+    public static final String command = "imagery";
+    public static final String permissionKey = "remotecontrol.permission.imagery";
+    public static final boolean permissionDefault = true;
+
+    @Override
+    public String getPermissionMessage() {
+        return tr("Remote Control has been asked to load an imagery layer from the following URL:") +
+        "<br>" + args.get("url");
+    }
+
+    @Override
+    protected String[] getMandatoryParams()
+    {
+        return new String[] { "url" };
+    }
+
+    @Override
+    public PermissionPrefWithDefault getPermissionPref()
+    {
+        return new PermissionPrefWithDefault(permissionKey, permissionDefault,
+        "RemoteControl: import forbidden by preferences");
+    }
+
+    @Override
+    protected void handleRequest() throws RequestHandlerErrorException {
+        if (Main.map == null) //Avoid exception when creating ImageryLayer with null MapFrame
+            throw new RequestHandlerErrorException();
+        String url = args.get("url");
+        String title = args.get("title");
+        if((title == null) || (title.length() == 0))
+        {
+            title = tr("Remote imagery");
+        }
+        String cookies = args.get("cookies");
+        ImageryLayer imgLayer = ImageryLayer.create(new ImageryInfo(title, url, cookies));
+        Main.main.addLayer(imgLayer);
+
+    }
+
+    @Override
+    public void parseArgs() {
+        StringTokenizer st = new StringTokenizer(request, "&?");
+        HashMap<String, String> args = new HashMap<String, String>();
+        // skip first element which is the command
+        if(st.hasMoreTokens()) {
+            st.nextToken();
+        }
+        while (st.hasMoreTokens()) {
+            String param = st.nextToken();
+            int eq = param.indexOf("=");
+            if (eq > -1)
+            {
+                String key = param.substring(0, eq);
+                /* "url=" terminates normal parameters
+                 * and will be handled separately
+                 */
+                if("url".equals(key)) {
+                    break;
+                }
+
+                String value = param.substring(eq + 1);
+                // urldecode all normal values
+                try {
+                    value = URLDecoder.decode(value, "UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    // TODO Auto-generated catch block
+                    e.printStackTrace();
+                }
+                args.put(key,
+                        value);
+            }
+        }
+        // url as second or later parameter
+        int urlpos = request.indexOf("&url=");
+        // url as first (and only) parameter
+        if(urlpos < 0) {
+            urlpos = request.indexOf("?url=");
+        }
+        // url found?
+        if(urlpos >= 0) {
+            // URL value
+            String value = request.substring(urlpos + 5);
+            // allow skipping URL decoding with urldecode=false
+            String urldecode = args.get("urldecode");
+            if((urldecode == null) || (Boolean.valueOf(urldecode) == true))
+            {
+                try {
+                    value = URLDecoder.decode(value, "UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    // TODO Auto-generated catch block
+                    e.printStackTrace();
+                }
+            }
+            args.put("url", value);
+        }
+        this.args = args;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java	(revision 3714)
+++ /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java	(revision 3715)
@@ -89,5 +89,7 @@
             {"usertools", IN_CORE},
             {"AgPifoJ", IN_CORE}, {"utilsplugin", IN_CORE}, {"ghost", IN_CORE},
-            {"validator", IN_CORE}, {"multipoly", IN_CORE}}) {
+            {"validator", IN_CORE}, {"multipoly", IN_CORE},
+            {"remotecontrol", IN_CORE},
+            {"imagery", IN_CORE}, {"slippymap", IN_CORE}, {"wmsplugin", IN_CORE}}) {
             DEPRECATED_PLUGINS.put(depr[0], depr.length >= 2 ? depr[1] : null);
         }
Index: /trunk/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java	(revision 3715)
+++ /trunk/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java	(revision 3715)
@@ -0,0 +1,30 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import java.io.File;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
+import org.openstreetmap.josm.data.imagery.OffsetBookmark;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginInformation;
+
+public class ImageryPlugin extends Plugin  {
+
+
+    public ImageryLayerInfo info = new ImageryLayerInfo();
+    // remember state of menu item to restore on changed preferences
+
+    public ImageryPlugin(PluginInformation info) {
+        super(info);
+        this.info.load();
+        OffsetBookmark.loadBookmarks();
+
+    }
+
+    @Override
+    public String getPluginDir()
+    {
+        return new File(Main.pref.getPluginsDirectory(), "imagery").getPath();
+    }
+
+}
