[josm-plugins] 199/369: Imported Upstream version 0.0.svn24572

Bas Couwenberg sebastic at xs4all.nl
Sat Oct 18 12:03:42 UTC 2014


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

sebastic-guest pushed a commit to branch master
in repository josm-plugins.

commit dfa9415d366b5a26458ef4c09463f6dd6cc79a1c
Author: David Paleino <dapal at debian.org>
Date:   Mon Dec 6 11:09:18 2010 +0100

    Imported Upstream version 0.0.svn24572
---
 DirectUpload/build.xml                             |   2 +-
 .../josm/plugins/DirectUpload/UploadDataGui.java   | 242 +++---
 .../plugins/DirectUpload/UploadDataGuiPlugin.java  |  49 +-
 .../plugins/DirectUpload/UploadOsmConnection.java  |  83 ++
 editgpx/.classpath                                 |   2 +-
 editgpx/.project                                   |   2 +-
 editgpx/.settings/org.eclipse.jdt.core.prefs       | 260 ++++++
 editgpx/.settings/org.eclipse.jdt.ui.prefs         |  56 +-
 editgpx/build.xml                                  |  11 +-
 .../josm/plugins/editgpx/EditGpxLayer.java         |   2 +-
 .../josm/plugins/editgpx/data/EditGpxData.java     |   4 +-
 .../josm/plugins/editgpx/data/EditGpxTrack.java    |   9 +-
 {editgpx => imagery}/.classpath                    |   4 +-
 {editgpx => imagery}/.project                      |   9 +-
 imagery/GPL-v2.0.txt                               | 339 ++++++++
 imagery/GPL-v3.0.txt                               | 674 +++++++++++++++
 imagery/README                                     |  27 +
 imagery/build.xml                                  | 258 ++++++
 imagery/images/OLmarker.png                        | Bin 0 -> 549 bytes
 imagery/images/bing_maps.png                       | Bin 0 -> 4295 bytes
 imagery/images/blankmenu.png                       | Bin 0 -> 1529 bytes
 imagery/images/cursor/modifier/move.png            | Bin 0 -> 192 bytes
 imagery/images/imagery.png                         | Bin 0 -> 9455 bytes
 imagery/images/imagery_menu.png                    | Bin 0 -> 1780 bytes
 imagery/images/imagery_small.png                   | Bin 0 -> 880 bytes
 imagery/images/load.png                            | Bin 0 -> 883 bytes
 imagery/images/mapmode/adjustimg.png               | Bin 0 -> 1515 bytes
 imagery/images/preferences/imagery.png             | Bin 0 -> 6831 bytes
 imagery/images/save.png                            | Bin 0 -> 845 bytes
 imagery/images_nodist/imagery.xcf                  | Bin 0 -> 17352 bytes
 imagery/sources.cfg                                |  69 ++
 .../plugins/imagery/AddImageryLayerAction.java     |  25 +
 .../josm/plugins/imagery/ImageryAdjustAction.java  | 207 +++++
 .../josm/plugins/imagery/ImageryInfo.java          | 185 ++++
 .../josm/plugins/imagery/ImageryLayer.java         |  81 ++
 .../josm/plugins/imagery/ImageryLayerInfo.java     | 104 +++
 .../josm/plugins/imagery/ImageryPlugin.java        | 273 ++++++
 .../plugins/imagery/ImageryPreferenceEditor.java   | 540 ++++++++++++
 .../josm/plugins/imagery/ImageryPreferences.java   |  28 +
 .../josm/plugins/imagery/ImageryRemoteHandler.java | 108 +++
 .../plugins/imagery/tms/BingAerialTileSource.java  | 191 +++++
 .../josm/plugins/imagery/tms/TMSKey.java           |  78 ++
 .../josm/plugins/imagery/tms/TMSLayer.java         | 319 +++++--
 .../josm/plugins/imagery/tms/TMSPreferences.java   |  82 ++
 .../josm/plugins/imagery/tms/TMSTileSource.java    |  22 +
 .../imagery/tms/TemplatedTMSTileSource.java        |  30 +
 .../josm/plugins/imagery/wms/AddWMSLayerPanel.java | 530 ++++++++++++
 .../josm/plugins/imagery/wms/GeorefImage.java      | 248 ++++++
 .../josm/plugins/imagery/wms/Grabber.java          | 115 +++
 .../josm/plugins/imagery/wms/HTMLGrabber.java      |  46 +
 .../imagery/wms/Map_Rectifier_WMSmenuAction.java   | 238 ++++++
 .../josm/plugins/imagery/wms/WMSAdapter.java       |  40 +
 .../josm/plugins/imagery/wms/WMSGrabber.java       | 203 +++++
 .../josm/plugins/imagery/wms}/WMSLayer.java        | 111 ++-
 .../josm/plugins/imagery/wms/WMSRequest.java       | 102 +++
 .../plugins/imagery/wms/io/WMSLayerExporter.java   |  13 +
 .../plugins/imagery/wms/io/WMSLayerImporter.java   |  14 +
 routing/build.xml                                  |   2 +-
 .../innovant/josm/plugin/routing/RoutingLayer.java |   3 +-
 .../routing/gui/RoutingPreferenceDialog.java       |   6 +-
 slippymap/.classpath                               |   2 +-
 slippymap/build.xml                                |   4 +-
 slippymap/images/bing_maps.png                     | Bin 0 -> 4295 bytes
 .../josm/plugins/slippymap/SlippyMapLayer.java     |  99 ++-
 .../josm/plugins/slippymap/SlippyMapPlugin.java    | 135 +--
 .../plugins/slippymap/SlippyMapPreferences.java    | 170 +++-
 svn-info.xml                                       |   8 +-
 wmsplugin/build.xml                                |   2 +-
 wmsplugin/sources.cfg                              |   3 +
 wmsplugin/src/wmsplugin/AddWMSLayerPanel.java      | 928 ++++++++++-----------
 wmsplugin/src/wmsplugin/WMSLayer.java              |   8 +
 wmsplugin/src/wmsplugin/WMSLayerInfo.java          |   3 +-
 72 files changed, 6523 insertions(+), 885 deletions(-)

diff --git a/DirectUpload/build.xml b/DirectUpload/build.xml
index 555c2c4..16c1ea7 100644
--- a/DirectUpload/build.xml
+++ b/DirectUpload/build.xml
@@ -26,7 +26,7 @@
 -->
 <project name="DirectUpload" default="dist" basedir=".">
 
-	<property name="commit.message" value="Changed constructor signature of plugin main class" />
+	<property name="commit.message" value="applied JOSM Ticket 4498 (patch by ax) - oauth support for gpx upload (I accidentally committed parts of the path in [24236])" />
 	<property name="plugin.main.version" value="3338" />
 
 	<property name="josm"                   location="../../core/dist/josm-custom.jar"/>
diff --git a/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGui.java b/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGui.java
index 29775e6..a1fed59 100644
--- a/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGui.java
+++ b/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGui.java
@@ -19,38 +19,34 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.Charset;
-import java.nio.charset.CharsetEncoder;
 import java.text.DecimalFormat;
 import java.text.SimpleDateFormat;
+import java.util.Collections;
 import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
 
 import javax.swing.JComboBox;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
-import javax.swing.JTextField;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.gpx.GpxData;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.JMultilineLabel;
-import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
-import org.openstreetmap.josm.gui.layer.GpxLayer;
-import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
 import org.openstreetmap.josm.io.GpxWriter;
-import org.openstreetmap.josm.tools.Base64;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.UrlLabel;
 
 /**
  *
- * @author  subhodip, xeen
+ * @author  subhodip, xeen, ax
  */
 public class UploadDataGui extends ExtendedDialog {
+    
     /**
      * This enum contains the possible values for the visibility field and their
      * explanation. Provides some methods for easier handling.
@@ -86,19 +82,14 @@ public class UploadDataGui extends ExtendedDialog {
         }
     }
 
-
-    // User for log in when uploading trace
-    private String username = Main.pref.get("osm-server.username");
-    private String password = Main.pref.get("osm-server.password");
-
     // Fields are declared here for easy access
     // Do not remove the space in JMultilineLabel. Otherwise the label will be empty
     // as we don't know its contents yet and therefore have a height of 0. This will
     // lead to unnecessary scrollbars.
     private JMultilineLabel OutputDisplay = new JMultilineLabel(" ");
-    private JTextField descriptionField = new JTextField(50);
-    private JTextField tagsField = new JTextField(50);
-    private JComboBox visibilityCombo = new JComboBox();
+    private HistoryComboBox descriptionField;
+    private HistoryComboBox tagsField;
+    private JComboBox visibilityCombo;
 
     // Constants used when generating upload request
     private static final String API_VERSION = "0.6";
@@ -106,12 +97,8 @@ public class UploadDataGui extends ExtendedDialog {
     private static final String LINE_END = "\r\n";
     private static final String uploadTraceText = tr("Upload Trace");
 
-    // Filename and current date. Date will be used as fallback if filename not available
-    private String datename = new SimpleDateFormat("yyMMddHHmmss").format(new Date());
-    private String filename = "";
-
     private boolean cancelled = false;
-
+    
     public UploadDataGui() {
         // Initalizes ExtendedDialog
         super(Main.parent,
@@ -120,12 +107,13 @@ public class UploadDataGui extends ExtendedDialog {
                 true
         );
         JPanel content = initComponents();
-        autoSelectTrace();
+        GpxData gpxData = UploadOsmConnection.getInstance().autoSelectTrace();
+        initTitleAndDescriptionFromGpxData(gpxData);    // this is changing some dialog elements, so it (probably) must be before the following  
         setContent(content);
         setButtonIcons(new String[] { "uploadtrace.png", "cancel.png" });
         setupDialog();
 
-        buttons.get(0).setEnabled(!checkForGPXLayer());
+        buttons.get(0).setEnabled(gpxData != null);
     }
 
     /**
@@ -133,19 +121,40 @@ public class UploadDataGui extends ExtendedDialog {
      * @return JPanel with components
      */
     private JPanel initComponents() {
+        // visibilty
         JLabel visibilityLabel = new JLabel(tr("Visibility"));
         visibilityLabel.setToolTipText(tr("Defines the visibility of your trace for other OSM users."));
+        
+        visibilityCombo = new JComboBox();
+        visibilityCombo.setEditable(false);
         for(visibility v : visibility.values()) {
             visibilityCombo.addItem(v.description);
         }
+        visibilityCombo.setSelectedItem(visibility.valueOf(Main.pref.get("directupload.visibility.last-used", visibility.PRIVATE.name())).description);
         UrlLabel visiUrl = new UrlLabel(tr("http://wiki.openstreetmap.org/wiki/Visibility_of_GPS_traces"), tr("(What does that mean?)"));
 
+        // description
         JLabel descriptionLabel = new JLabel(tr("Description"));
+        descriptionField = new HistoryComboBox();
         descriptionField.setToolTipText(tr("Please enter Description about your trace."));
-
+        
+        List<String> descHistory = new LinkedList<String>(Main.pref.getCollection("directupload.description.history", new LinkedList<String>()));
+        // we have to reverse the history, because ComboBoxHistory will reverse it againin addElement()
+        // XXX this should be handled in HistoryComboBox
+        Collections.reverse(descHistory);
+        descriptionField.setPossibleItems(descHistory);
+
+        // tags
         JLabel tagsLabel = new JLabel(tr("Tags (comma delimited)"));
+        tagsField = new HistoryComboBox();
         tagsField.setToolTipText(tr("Please enter tags about your trace."));
 
+        List<String> tagsHistory = new LinkedList<String>(Main.pref.getCollection("directupload.tags.history", new LinkedList<String>()));
+        // we have to reverse the history, because ComboBoxHistory will reverse it againin addElement()
+        // XXX this should be handled in HistoryComboBox
+        Collections.reverse(tagsHistory);
+        tagsField.setPossibleItems(tagsHistory);
+
         JPanel p = new JPanel(new GridBagLayout());
 
         OutputDisplay.setMaxWidth(findMaxDialogSize().width-10);
@@ -164,36 +173,18 @@ public class UploadDataGui extends ExtendedDialog {
         return p;
     }
 
-    /**
-     * This function will automatically select a GPX layer if it's the only one.
-     * If a GPX layer is found, its filename will be parsed and displayed
-     */
-    private void autoSelectTrace() {
-        // If no GPX layer is selected, select one for the user if there is only one GPX layer
-        if(Main.map != null && Main.map.mapView != null) {
-            MapView mv=Main.map.mapView;
-            if(!(mv.getActiveLayer() instanceof GpxLayer)) {
-                Layer lastLayer=null;
-                int layerCount=0;
-                for (Layer l : mv.getAllLayers()) {
-                    if(l instanceof GpxLayer) {
-                        lastLayer = l;
-                        layerCount++;
-                    }
-                }
-                if(layerCount == 1) mv.setActiveLayer(lastLayer);
-            }
-
-            if(mv.getActiveLayer() instanceof GpxLayer) {
-                GpxData data=((GpxLayer)Main.map.mapView.getActiveLayer()).data;
-                try {
-                    filename = data.storageFile.getName()
-                                    .replaceAll("[&?/\\\\]"," ").replaceAll("(\\.[^.]*)$","");
-                } catch(Exception e) { }
-                descriptionField.setText(getFilename());
-                OutputDisplay.setText(tr("Selected track: {0}", getFilename()));
-            }
-        }
+    private void initTitleAndDescriptionFromGpxData(GpxData gpxData) {
+      String description, title;
+      try {
+          description = gpxData.storageFile.getName().replaceAll("[&?/\\\\]"," ").replaceAll("(\\.[^.]*)$","");
+          title = tr("Selected track: {0}", gpxData.storageFile.getName());
+      }
+      catch(Exception e) {
+          description = new SimpleDateFormat("yyMMddHHmmss").format(new Date()); 
+          title = tr("No GPX layer selected. Cannot upload a trace.");
+      }
+      OutputDisplay.setText(title);
+      descriptionField.setText(description);
     }
 
     /**
@@ -204,53 +195,54 @@ public class UploadDataGui extends ExtendedDialog {
      * @param GpxData The GPX Data to upload
      */
     private void upload(String description, String tags, String visi, GpxData gpxData, ProgressMonitor progressMonitor) throws IOException {
-        progressMonitor.beginTask(null);
+        progressMonitor.beginTask(tr("Uploading trace ..."));
         try {
-            if(checkForErrors(username, password, description, gpxData))
+            if (checkForErrors(description, gpxData)) {
                 return;
+            }
 
             // Clean description/tags from disallowed chars
-            description = description.replaceAll("[&?/\\\\]"," ");
-            tags = tags.replaceAll("[&?/\\\\.;]"," ");
+            description = description.replaceAll("[&?/\\\\]", " ");
+            tags = tags.replaceAll("[&?/\\\\.;]", " ");
 
             // Set progress dialog to indeterminate while connecting
             progressMonitor.indeterminateSubTask(tr("Connecting..."));
 
-            try {
-                // Generate data for upload
-                ByteArrayOutputStream baos  = new ByteArrayOutputStream();
-                writeGpxFile(baos, "file", gpxData);
-                writeField(baos, "description", description);
-                writeField(baos, "tags", (tags != null && tags.length() > 0) ? tags : "");
-                writeField(baos, "visibility", visi);
-                writeString(baos, "--" + BOUNDARY + "--" + LINE_END);
-
-                ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-                HttpURLConnection conn = setupConnection(baos.size());
-
-                progressMonitor.setTicksCount(baos.size());
-                progressMonitor.subTask(null);
-
-                try {
-                    flushToServer(bais, conn.getOutputStream(), progressMonitor);
-                } catch(Exception e) {}
-
-                if(cancelled) {
-                    conn.disconnect();
-                    OutputDisplay.setText(tr("Upload cancelled"));
-                    buttons.get(0).setEnabled(true);
-                    cancelled = false;
-                } else {
-                    boolean success = finishUpConnection(conn);
-                    buttons.get(0).setEnabled(!success);
-                    if(success)
-                        buttons.get(1).setText(tr("Close"));
+            // Generate data for upload
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            writeGpxFile(baos, "file", gpxData);
+            writeField(baos, "description", description);
+            writeField(baos, "tags", (tags != null && tags.length() > 0) ? tags : "");
+            writeField(baos, "visibility", visi);
+            writeString(baos, "--" + BOUNDARY + "--" + LINE_END);
+
+            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+            HttpURLConnection conn = setupConnection(baos.size());
+
+            progressMonitor.setTicksCount(baos.size());
+            progressMonitor.subTask(null);
+
+            flushToServer(bais, conn.getOutputStream(), progressMonitor);
+
+            if (cancelled) {
+                conn.disconnect();
+                OutputDisplay.setText(tr("Upload cancelled"));
+                buttons.get(0).setEnabled(true);
+                cancelled = false;
+            }
+            else {
+                boolean success = finishUpConnection(conn);
+                buttons.get(0).setEnabled(!success);
+                if (success) {
+                    buttons.get(1).setText(tr("Close"));
                 }
-            } catch(Exception e) {
-                OutputDisplay.setText(tr("Error while uploading"));
-                e.printStackTrace();
             }
-        } finally {
+        }
+        catch (Exception e) {
+            OutputDisplay.setText(tr("Error while uploading"));
+            e.printStackTrace();
+        }
+        finally {
             progressMonitor.finishTask();
         }
     }
@@ -262,10 +254,6 @@ public class UploadDataGui extends ExtendedDialog {
      * @return HttpURLConnection The set up conenction
      */
     private HttpURLConnection setupConnection(int contentLength) throws Exception {
-        // Encode username and password
-        CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
-        String auth = username + ":" + password;
-        ByteBuffer bytes = encoder.encode(CharBuffer.wrap(auth));
 
         // Upload URL
         URL url = new URL("http://www.openstreetmap.org/api/" + API_VERSION + "/gpx/create");
@@ -276,7 +264,10 @@ public class UploadDataGui extends ExtendedDialog {
         c.setConnectTimeout(15000);
         c.setRequestMethod("POST");
         c.setDoOutput(true);
-        c.addRequestProperty("Authorization", "Basic " + Base64.encode(bytes));
+        // unfortunately, addAuth() is protected, so we need to subclass OsmConnection 
+        // XXX make addAuth public. 
+        UploadOsmConnection.getInstance().addAuthHack(c);
+
         c.addRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
         c.addRequestProperty("Connection", "close"); // counterpart of keep-alive
         c.addRequestProperty("Expect", "");
@@ -366,14 +357,11 @@ public class UploadDataGui extends ExtendedDialog {
     /**
      * Checks for common errors and displays them in OutputDisplay if it finds any.
      * Returns whether errors have been found or not.
-     * @param String OSM username
-     * @param String OSM password
      * @param String GPX track description
      * @param GpxData the GPX data to upload
      * @return boolean true if errors have been found
      */
-    private boolean checkForErrors(String username, String password,
-                                   String description, GpxData gpxData) {
+    private boolean checkForErrors(String description, GpxData gpxData) {
         String errors="";
         if(description == null || description.length() == 0)
             errors += tr("No description provided. Please provide some description.");
@@ -381,48 +369,37 @@ public class UploadDataGui extends ExtendedDialog {
         if(gpxData == null)
             errors += tr("No GPX layer selected. Cannot upload a trace.");
 
-        if(username == null || username.length() == 0)
-            errors += tr("No username provided.");
-
-        if(password == null || password.length() == 0)
-            errors += tr("No password provided.");
-
         OutputDisplay.setText(errors);
         return errors.length() > 0;
     }
 
     /**
-     * Checks if a GPX layer is selected and returns the result. Also writes an error
-     * message to OutputDisplay if result is false.
-     * @return boolean True, if /no/ GPX layer is selected
-     */
-    private boolean checkForGPXLayer() {
-        if(Main.map == null
-                || Main.map.mapView == null
-                || Main.map.mapView.getActiveLayer() == null
-                || !(Main.map.mapView.getActiveLayer() instanceof GpxLayer)) {
-            OutputDisplay.setText(tr("No GPX layer selected. Cannot upload a trace."));
-            return true;
-        }
-        return false;
-    }
-
-
-    /**
      * This creates the uploadTask that does the actual work and hands it to the main.worker to be executed.
      */
     private void setupUpload() {
-        if(checkForGPXLayer()) return;
+        final GpxData gpxData = UploadOsmConnection.getInstance().autoSelectTrace();
+        if (gpxData == null) {
+            return;
+        }
 
         // Disable Upload button so users can't just upload that track again
         buttons.get(0).setEnabled(false);
+        
+        // save history
+        Main.pref.put("directupload.visibility.last-used", visibility.desc2visi(visibilityCombo.getSelectedItem().toString()).name());
+        
+        descriptionField.addCurrentItemToHistory();
+        Main.pref.putCollection("directupload.description.history", descriptionField.getHistory());
+
+        tagsField.addCurrentItemToHistory();
+        Main.pref.putCollection("directupload.tags.history", tagsField.getHistory());
 
         PleaseWaitRunnable uploadTask = new PleaseWaitRunnable(tr("Uploading GPX Track")){
             @Override protected void realRun() throws IOException {
                   upload(descriptionField.getText(),
                          tagsField.getText(),
                          visibility.desc2visi(visibilityCombo.getSelectedItem()).toString(),
-                         ((GpxLayer)Main.map.mapView.getActiveLayer()).data,
+                         gpxData,
                          progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)
                   );
             }
@@ -459,7 +436,7 @@ public class UploadDataGui extends ExtendedDialog {
     private void writeGpxFile(ByteArrayOutputStream baos, String name, GpxData gpxData) throws IOException {
         writeBoundary(baos);
         writeString(baos, "Content-Disposition: form-data; name=\"" + name + "\"; ");
-        writeString(baos, "filename=\"" + getFilename() + ".gpx" + "\"");
+        writeString(baos, "filename=\"" + gpxData.storageFile.getName() + "\"");
         writeLineEnd(baos);
         writeString(baos, "Content-Type: application/octet-stream");
         writeLineEnd(baos);
@@ -497,15 +474,6 @@ public class UploadDataGui extends ExtendedDialog {
     }
 
     /**
-     * Returns the filename of the GPX file to be upload. If not available, returns current date
-     * as an alternative
-     * @param String
-     */
-    private String getFilename() {
-       return filename.equals("") ? datename : filename;
-    }
-
-    /**
      * Overrides the default actions. Will not close the window when upload trace is clicked
      */
     @Override protected void buttonAction(int buttonIndex, ActionEvent evt) {
diff --git a/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGuiPlugin.java b/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGuiPlugin.java
index 0fb7e68..e7d0008 100644
--- a/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGuiPlugin.java
+++ b/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGuiPlugin.java
@@ -10,19 +10,19 @@ import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
-import java.util.List;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.JosmAction;
-import org.openstreetmap.josm.gui.layer.GpxLayer;
 import org.openstreetmap.josm.plugins.Plugin;
 import org.openstreetmap.josm.plugins.PluginInformation;
 import org.openstreetmap.josm.tools.Shortcut;
+
 /**
  *
- * @author subhodip
+ * @author subhodip, ax
  */
-public class UploadDataGuiPlugin extends Plugin{
+public class UploadDataGuiPlugin extends Plugin {
+    
     UploadAction openaction;
 
     public UploadDataGuiPlugin(PluginInformation info) {
@@ -31,35 +31,30 @@ public class UploadDataGuiPlugin extends Plugin{
         Main.main.menu.toolsMenu.add(openaction);
     }
 
-    class UploadAction extends JosmAction{
+    class UploadAction extends JosmAction {
+        
         public UploadAction(){
             super(tr("Upload Traces"), "UploadAction", tr("Uploads traces to openstreetmap.org"),
-            Shortcut.registerShortcut("tools:uploadtraces", tr("Tool: {0}", tr("Upload Traces")),
-            KeyEvent.VK_G, Shortcut.GROUP_MENU), false);
+                Shortcut.registerShortcut("tools:uploadtraces", tr("Tool: {0}", tr("Upload Traces")),
+                KeyEvent.VK_G, Shortcut.GROUP_MENU), false);
         }
+        
         public void actionPerformed(ActionEvent e) {
             UploadDataGui go = new UploadDataGui();
             go.setVisible(true);
         }
 
-        @Override
-        protected void updateEnabledState() {
-            // enable button if there is "one active GpxLayer" or "exactly one GpxLayer in the list of all layers available"
-            if(Main.map == null
-                    || Main.map.mapView == null
-                    || Main.map.mapView.getActiveLayer() == null
-                    || !(Main.map.mapView.getActiveLayer() instanceof GpxLayer)) {
-                setEnabled(false);
-            } else {
-                setEnabled(true);
-            }
-
-            if(Main.map != null && Main.map.mapView.getNumLayers() > 1) {
-                List<GpxLayer> list = Main.map.mapView.getLayersOfType(GpxLayer.class);
-                if (list.size() == 1)
-                    setEnabled(true);
-            }
-
-        }
+        // because LayerListDialog doesn't provide a way to hook into "layer selection changed"
+        // but the layer selection (*not* activation) is how we determine the layer to be uploaded
+        // we have to let the upload trace menu always be enabled
+//        @Override
+//        protected void updateEnabledState() {
+//            // enable button if ... @see autoSelectTrace()
+//            if (UploadOsmConnection.getInstance().autoSelectTrace() == null) {
+//                setEnabled(false);
+//            } else {
+//                setEnabled(true);
+//            }
+//        }
     }
-}
\ No newline at end of file
+}
diff --git a/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadOsmConnection.java b/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadOsmConnection.java
new file mode 100644
index 0000000..e4622b4
--- /dev/null
+++ b/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadOsmConnection.java
@@ -0,0 +1,83 @@
+// ...
+
+package org.openstreetmap.josm.plugins.DirectUpload;
+
+import java.net.HttpURLConnection;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.io.OsmConnection;
+import org.openstreetmap.josm.io.OsmTransferException;
+
+/**
+ * Work-around and utility class for DirectUpload.
+ * 
+ * @author ax
+ */
+public class UploadOsmConnection extends OsmConnection {
+
+	// singleton, see http://en.wikipedia.org/wiki/Singleton_pattern#Traditional_simple_way
+	private static final UploadOsmConnection INSTANCE = new UploadOsmConnection();
+
+	// Private constructor prevents instantiation from other classes
+	private UploadOsmConnection() {
+	}
+
+	public static UploadOsmConnection getInstance() {
+		return UploadOsmConnection.INSTANCE;
+	}
+
+	// make protected OsmConnection::addAuth() available to others
+	public void addAuthHack(HttpURLConnection connection) throws OsmTransferException {
+		addAuth(connection);
+	}
+
+    /**
+     * find which gpx layer holds the trace to upload. layers are tried in this order:
+     * 
+     * 1. selected (*not* active - think "zoom to layer"), from first to last
+     * 2. not selectd - if there is only one 
+     * 3. active
+     *
+     * @return data of the selected gpx layer, or null if there is none
+     */
+    GpxData autoSelectTrace() {
+        if (Main.map != null && Main.map.mapView != null) {
+            MapView mv = Main.map.mapView;
+//            List<Layer> allLayers = new ArrayList<Layer>(mv.getAllLayersAsList());  // modifiable
+            List<Layer> selectedLayers = LayerListDialog.getInstance().getModel().getSelectedLayers();
+            List<GpxLayer> gpxLayersRemaining = mv.getLayersOfType(GpxLayer.class);
+            gpxLayersRemaining.removeAll(selectedLayers);
+            GpxLayer traceLayer = null;
+            // find the first gpx layer inside selected layers
+            for (Layer l : LayerListDialog.getInstance().getModel().getSelectedLayers()) {
+                if (l instanceof GpxLayer) {
+                    traceLayer = (GpxLayer) l;
+                    break;
+                }
+            }
+            if (traceLayer == null) {
+                // if there is none, try the none selected gpx layers. if there is only one, use it.
+                if (gpxLayersRemaining.size() == 1) {
+                    traceLayer = gpxLayersRemaining.get(0); 
+                }
+                // active layer
+                else if (mv.getActiveLayer() instanceof GpxLayer) {
+                    traceLayer = (GpxLayer) mv.getActiveLayer(); 
+                }
+            }
+
+            if (traceLayer != null) {
+                GpxData data = traceLayer.data;
+                return data;
+            }
+        }
+        
+        return null;
+    }
+}
diff --git a/editgpx/.classpath b/editgpx/.classpath
index 4a8596f..d4528ee 100644
--- a/editgpx/.classpath
+++ b/editgpx/.classpath
@@ -2,7 +2,7 @@
 <classpath>
 	<classpathentry kind="src" path="src"/>
 	<classpathentry excluding="src/" including="images/" kind="src" path=""/>
-	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JDK 5"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/JOSM"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/editgpx/.project b/editgpx/.project
index 569f47e..3142f1a 100644
--- a/editgpx/.project
+++ b/editgpx/.project
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <projectDescription>
-	<name>editgpx</name>
+	<name>editgpx_svn</name>
 	<comment></comment>
 	<projects>
 	</projects>
diff --git a/editgpx/.settings/org.eclipse.jdt.core.prefs b/editgpx/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..7bf9d9c
--- /dev/null
+++ b/editgpx/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,260 @@
+#Sat Oct 16 19:18:33 CEST 2010
+eclipse.preferences.version=1
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=0
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=false
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=4
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/editgpx/.settings/org.eclipse.jdt.ui.prefs b/editgpx/.settings/org.eclipse.jdt.ui.prefs
index 4a3398d..079a653 100644
--- a/editgpx/.settings/org.eclipse.jdt.ui.prefs
+++ b/editgpx/.settings/org.eclipse.jdt.ui.prefs
@@ -1,6 +1,60 @@
-#Tue May 25 21:33:38 CEST 2010
+#Sat Oct 16 19:07:29 CEST 2010
+cleanup.add_default_serial_version_id=true
+cleanup.add_generated_serial_version_id=false
+cleanup.add_missing_annotations=true
+cleanup.add_missing_deprecated_annotations=true
+cleanup.add_missing_methods=false
+cleanup.add_missing_nls_tags=false
+cleanup.add_missing_override_annotations=true
+cleanup.add_serial_version_id=false
+cleanup.always_use_blocks=true
+cleanup.always_use_parentheses_in_expressions=false
+cleanup.always_use_this_for_non_static_field_access=false
+cleanup.always_use_this_for_non_static_method_access=false
+cleanup.convert_to_enhanced_for_loop=false
+cleanup.correct_indentation=false
+cleanup.format_source_code=false
+cleanup.format_source_code_changes_only=false
+cleanup.make_local_variable_final=true
+cleanup.make_parameters_final=false
+cleanup.make_private_fields_final=true
+cleanup.make_type_abstract_if_missing_method=false
+cleanup.make_variable_declarations_final=false
+cleanup.never_use_blocks=false
+cleanup.never_use_parentheses_in_expressions=true
+cleanup.organize_imports=false
+cleanup.qualify_static_field_accesses_with_declaring_class=false
+cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+cleanup.qualify_static_member_accesses_with_declaring_class=true
+cleanup.qualify_static_method_accesses_with_declaring_class=false
+cleanup.remove_private_constructors=true
+cleanup.remove_trailing_whitespaces=false
+cleanup.remove_trailing_whitespaces_all=true
+cleanup.remove_trailing_whitespaces_ignore_empty=false
+cleanup.remove_unnecessary_casts=true
+cleanup.remove_unnecessary_nls_tags=true
+cleanup.remove_unused_imports=true
+cleanup.remove_unused_local_variables=false
+cleanup.remove_unused_private_fields=true
+cleanup.remove_unused_private_members=false
+cleanup.remove_unused_private_methods=true
+cleanup.remove_unused_private_types=true
+cleanup.sort_members=false
+cleanup.sort_members_all=false
+cleanup.use_blocks=false
+cleanup.use_blocks_only_for_return_and_throw=false
+cleanup.use_parentheses_in_expressions=false
+cleanup.use_this_for_non_static_field_access=false
+cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+cleanup.use_this_for_non_static_method_access=false
+cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+cleanup_profile=org.eclipse.jdt.ui.default.eclipse_clean_up_profile
+cleanup_settings_version=2
 eclipse.preferences.version=1
 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Eclipse [built-in] + spaces for indent
+formatter_settings_version=11
 sp_cleanup.add_default_serial_version_id=true
 sp_cleanup.add_generated_serial_version_id=false
 sp_cleanup.add_missing_annotations=true
diff --git a/editgpx/build.xml b/editgpx/build.xml
index 326bed0..2801816 100644
--- a/editgpx/build.xml
+++ b/editgpx/build.xml
@@ -21,7 +21,7 @@
 **    set the properties commit.message and plugin.main.version
 ** and run
 **    > ant  publish
-**
+** 
 **
 -->
 <project name="editgpx" default="dist" basedir=".">
@@ -31,8 +31,10 @@
 	<property name="plugin.main.version" value="3408" />
 
 
-	<property name="josm"                   location="../../core/dist/josm-custom.jar"/>
-	<property name="plugin.dist.dir"        value="../../dist"/>
+	<!-- <property name="josm"                   location="../../core/dist/josm-custom.jar"/> -->
+	<property name="josm"                   location="../JOSM/dist/josm-custom.jar"/>
+	<!-- <property name="plugin.dist.dir"        value="../../dist"/>-->
+	<property name="plugin.dist.dir"        value=".."/>
 	<property name="plugin.build.dir"       value="build"/>
 	<property name="plugin.jar"             value="${plugin.dist.dir}/${ant.project.name}.jar"/>
 	<property name="ant.build.javac.target" value="1.5"/>
@@ -100,7 +102,8 @@
 			<env key="LANG" value="C"/>
 			<arg value="info"/>
 			<arg value="--xml"/>
-			<arg value="../../core"/>
+			<!-- <arg value="../../core"/>-->
+			<arg value="../JOSM"/>
 		</exec>
 		<xmlproperty file="core.info.xml" prefix="coreversion" keepRoot="true" collapseAttributes="true"/>
 		<echo>Building against core revision ${coreversion.info.entry.revision}.</echo>
diff --git a/editgpx/src/org/openstreetmap/josm/plugins/editgpx/EditGpxLayer.java b/editgpx/src/org/openstreetmap/josm/plugins/editgpx/EditGpxLayer.java
index 0b471a9..cb5be17 100644
--- a/editgpx/src/org/openstreetmap/josm/plugins/editgpx/EditGpxLayer.java
+++ b/editgpx/src/org/openstreetmap/josm/plugins/editgpx/EditGpxLayer.java
@@ -136,7 +136,7 @@ public class EditGpxLayer extends Layer {
      * @return GPXData
      */
     private GpxData toGpxData(boolean anonTime) {
-        return data.createGpxData();
+        return data.createGpxData(anonTime);
     }
 
     //context item "Convert to GPX layer"
diff --git a/editgpx/src/org/openstreetmap/josm/plugins/editgpx/data/EditGpxData.java b/editgpx/src/org/openstreetmap/josm/plugins/editgpx/data/EditGpxData.java
index 3c89c5e..c0b2395 100644
--- a/editgpx/src/org/openstreetmap/josm/plugins/editgpx/data/EditGpxData.java
+++ b/editgpx/src/org/openstreetmap/josm/plugins/editgpx/data/EditGpxData.java
@@ -42,12 +42,12 @@ public class EditGpxData {
         return tracks;
     }
 
-    public GpxData createGpxData() {
+    public GpxData createGpxData(boolean anonTime) {
         GpxData result = new GpxData();
 
         for (EditGpxTrack track: tracks) {
             if (!track.isDeleted()) {
-                GpxTrack newTrack = track.createGpxTrack();
+                GpxTrack newTrack = track.createGpxTrack(anonTime);
                 if (!newTrack.getSegments().isEmpty()) {
                     result.tracks.add(newTrack);
                 }
diff --git a/editgpx/src/org/openstreetmap/josm/plugins/editgpx/data/EditGpxTrack.java b/editgpx/src/org/openstreetmap/josm/plugins/editgpx/data/EditGpxTrack.java
index a539863..7388e3a 100644
--- a/editgpx/src/org/openstreetmap/josm/plugins/editgpx/data/EditGpxTrack.java
+++ b/editgpx/src/org/openstreetmap/josm/plugins/editgpx/data/EditGpxTrack.java
@@ -31,7 +31,7 @@ public class EditGpxTrack {
         return attributes;
     }
 
-    public GpxTrack createGpxTrack() {
+    public GpxTrack createGpxTrack(boolean anonTime) {
 
         Collection<Collection<WayPoint>> wayPoints = new ArrayList<Collection<WayPoint>>();
 
@@ -39,6 +39,13 @@ public class EditGpxTrack {
             if (!segment.isDeleted()) {
                 List<WayPoint> points = segment.getNonDeletedWaypoints();
                 if (!points.isEmpty()) {
+                    if (anonTime) {
+                        // convert to anonymous time
+                        for (WayPoint w : points) {
+                            w.attr.put("time", "1970-00-00T00:00:00.000Z");
+                            w.setTime();
+                        }
+                    }
                     wayPoints.add(points);
                 }
             }
diff --git a/editgpx/.classpath b/imagery/.classpath
similarity index 65%
copy from editgpx/.classpath
copy to imagery/.classpath
index 4a8596f..560d90f 100644
--- a/editgpx/.classpath
+++ b/imagery/.classpath
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
 	<classpathentry kind="src" path="src"/>
-	<classpathentry excluding="src/" including="images/" kind="src" path=""/>
-	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JDK 5"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/JOSM"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/remotecontrol"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/editgpx/.project b/imagery/.project
similarity index 65%
copy from editgpx/.project
copy to imagery/.project
index 569f47e..d92129d 100644
--- a/editgpx/.project
+++ b/imagery/.project
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <projectDescription>
-	<name>editgpx</name>
+	<name>imagery</name>
 	<comment></comment>
 	<projects>
 	</projects>
@@ -14,11 +14,4 @@
 	<natures>
 		<nature>org.eclipse.jdt.core.javanature</nature>
 	</natures>
-	<linkedResources>
-		<link>
-			<name>src-common</name>
-			<type>2</type>
-			<location>/Users/leo/dev/josm/core/src</location>
-		</link>
-	</linkedResources>
 </projectDescription>
diff --git a/imagery/GPL-v2.0.txt b/imagery/GPL-v2.0.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/imagery/GPL-v2.0.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/imagery/GPL-v3.0.txt b/imagery/GPL-v3.0.txt
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/imagery/GPL-v3.0.txt
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/imagery/README b/imagery/README
new file mode 100644
index 0000000..dbfb2d0
--- /dev/null
+++ b/imagery/README
@@ -0,0 +1,27 @@
+This plugin is an union of slippymap plugin and wmsplugin.
+Combined by Upliner, licensed under the GNU GPL v2 or later.
+
+WMSPlugin authors:
+==========================================================================
+This plugin has been created by tim <chippy2005 at gmail.com>
+and has received major contributions from Frederik Ramm
+<frederik at remote.org>. It is based on the "Landsat" plugin
+by Nick Whitelegg <Nick.Whitelegg at solent.ac.uk> and includes
+some code from Jonathan Stott <jonathan at jstott.me.uk>, Gabriel Ebner
+<ge at gabrielebner.at> and Ulf Lamping <ulf.lamping at web.de>.
+The automatic tiles downloading and Yahoo downloader made by Petr Dlouhý <petr.dlouhy at email.cz>
+
+This plugin is licensed under the GNU GPL v2 or later.
+==========================================================================
+
+Slippymap plugin authors:
+==========================================================================
+A plugin for displaying a slippy map grid, with various server interaction
+options (download tiles, request tile updates etc.)
+
+Author: Frederik Ramm <frederik at remote.org>
+        Lubomir Varga <lubomir.varga at freemap.sk> or <luvar at plaintext.sk>
+Public Domain.
+
+Software with a little bit of customisation, fade background feature, autozoom, autoload tiles e.t.c. Just a begining of the best plugin for josm ;-)
+==========================================================================
diff --git a/imagery/build.xml b/imagery/build.xml
new file mode 100644
index 0000000..54c54e8
--- /dev/null
+++ b/imagery/build.xml
@@ -0,0 +1,258 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+** This is a template build file for a JOSM  plugin.
+**
+** Maintaining versions
+** ====================
+** see README.template
+**
+** Usage
+** =====
+** To build it run
+**
+**    > ant  dist
+**
+** To install the generated plugin locally (in you default plugin directory) run
+**
+**    > ant  install
+**
+** The generated plugin jar is not automatically available in JOSMs plugin configuration
+** dialog. You have to check it in first.
+**
+** Use the ant target 'publish' to check in the plugin and make it available to other
+** JOSM users:
+**    set the properties commit.message and plugin.main.version
+** and run
+**    > ant  publish
+**
+**
+-->
+<project name="imagery" default="dist" basedir=".">
+
+    <!-- enter the SVN commit message -->
+    <property name="commit.message" value="Commit message" />
+    <!-- enter the *lowest* JOSM version this plugin is currently compatible with -->
+    <property name="plugin.main.version" value="3687" />
+
+
+    <!--
+      ************************************************
+      ** should not be necessary to change the following properties
+     -->
+    <property name="josm"                   location="../../core/dist/josm-custom.jar"/>
+    <property name="remotecontrol" location="../../dist/remotecontrol.jar" />
+    <property name="plugin.build.dir"       value="build"/>
+    <property name="plugin.src.dir"         value="src"/>
+    <!-- this is the directory where the plugin jar is copied to -->
+    <property name="plugin.dist.dir"        value="../../dist"/>
+    <property name="ant.build.javac.target" value="1.5"/>
+    <property name="plugin.dist.dir"        value="../../dist"/>
+    <property name="plugin.jar"             value="${plugin.dist.dir}/${ant.project.name}.jar"/>
+
+    <!--
+    **********************************************************
+    ** init - initializes the build
+    **********************************************************
+    -->
+    <target name="init">
+        <mkdir dir="${plugin.build.dir}"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** compile - complies the source tree
+    **********************************************************
+    -->
+    <target name="compile" depends="init">
+        <echo message="compiling sources for  ${plugin.jar} ... "/>
+        <javac srcdir="src" classpath="${josm};${remotecontrol}" debug="true" destdir="${plugin.build.dir}">
+            <compilerarg value="-Xlint:deprecation"/>
+            <compilerarg value="-Xlint:unchecked"/>
+        </javac>
+    </target>
+
+    <!--
+    **********************************************************
+    ** dist - creates the plugin jar
+    **********************************************************
+    -->
+    <target name="dist" depends="compile,revision">
+        <echo message="creating ${ant.project.name}.jar ... "/>
+        <copy todir="${plugin.build.dir}/resources">
+            <fileset dir="resources"/>
+        </copy>
+        <copy todir="${plugin.build.dir}/images">
+            <fileset dir="images"/>
+        </copy>
+        <copy todir="${plugin.build.dir}">
+            <fileset dir=".">
+                <include name="README" />
+                <include name="LICENSE" />
+            </fileset>
+        </copy>
+        <jar destfile="${plugin.jar}" basedir="${plugin.build.dir}">
+            <!--
+        ************************************************
+        ** configure these properties. Most of them will be copied to the plugins
+        ** manifest file. Property values will also show up in the list available
+        ** plugins: http://josm.openstreetmap.de/wiki/Plugins.
+        **
+        ************************************************
+    -->
+            <manifest>
+                <attribute name="Author" value="Tim Waters, Petr Dlouhý, Frederik Ramm, Upliner and others"/>
+                <attribute name="Plugin-Class" value="org.openstreetmap.josm.plugins.imagery.ImageryPlugin"/>
+                <attribute name="Plugin-Date" value="${version.entry.commit.date}"/>
+                <attribute name="Plugin-Description" value="Experimental union of SlippyMap plugin and WMSPlugin"/>
+                <attribute name="Plugin-Icon" value="images/imagery.png"/>
+                <attribute name="Plugin-Link" value="http://wiki.openstreetmap.org/wiki/JOSM/Plugins/WMSPlugin" />
+                <attribute name="Plugin-Mainversion" value="${plugin.main.version}"/>
+                <attribute name="Plugin-Version" value="${version.entry.commit.revision}"/>
+            </manifest>
+        </jar>
+    </target>
+
+    <!--
+    **********************************************************
+    ** revision - extracts the current revision number for the
+    **    file build.number and stores it in the XML property
+    **    version.*
+    **********************************************************
+    -->
+    <target name="revision">
+
+        <exec append="false" output="REVISION" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="info"/>
+            <arg value="--xml"/>
+            <arg value="."/>
+        </exec>
+        <xmlproperty file="REVISION" prefix="version" keepRoot="false" collapseAttributes="true"/>
+        <delete file="REVISION"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** clean - clean up the build environment
+    **********************************************************
+    -->
+    <target name="clean">
+        <delete dir="${plugin.build.dir}"/>
+        <delete file="${plugin.jar}"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** install - install the plugin in your local JOSM installation
+    **********************************************************
+    -->
+    <target name="install" depends="dist">
+        <property environment="env"/>
+        <condition property="josm.plugins.dir" value="${env.APPDATA}/JOSM/plugins" else="${user.home}/.josm/plugins">
+            <and>
+                <os family="windows"/>
+            </and>
+        </condition>
+        <copy file="${plugin.jar}" todir="${josm.plugins.dir}"/>
+    </target>
+
+
+    <!--
+    ************************** Publishing the plugin *********************************** 
+    -->
+    <!--
+        ** extracts the JOSM release for the JOSM version in ../core and saves it in the 
+        ** property ${coreversion.info.entry.revision}
+        **
+        -->
+    <target name="core-info">
+        <exec append="false" output="core.info.xml" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="info"/>
+            <arg value="--xml"/>
+            <arg value="../../core"/>
+        </exec>
+        <xmlproperty file="core.info.xml" prefix="coreversion" keepRoot="true" collapseAttributes="true"/>
+        <echo>Building against core revision ${coreversion.info.entry.revision}.</echo>
+        <echo>Plugin-Mainversion is set to ${plugin.main.version}.</echo>
+        <delete file="core.info.xml" />
+    </target>
+
+    <!--
+        ** commits the source tree for this plugin
+        -->
+    <target name="commit-current">
+        <echo>Commiting the plugin source with message '${commit.message}' ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="commit"/>
+            <arg value="-m '${commit.message}'"/>
+            <arg value="."/>
+        </exec>
+    </target>
+
+    <!--
+        ** updates (svn up) the source tree for this plugin
+        -->
+    <target name="update-current">
+        <echo>Updating plugin source ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="up"/>
+            <arg value="."/>
+        </exec>
+        <echo>Updating ${plugin.jar} ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="up"/>
+            <arg value="../dist/${plugin.jar}"/>
+        </exec>
+    </target>
+
+    <!--
+        ** commits the plugin.jar 
+        -->
+    <target name="commit-dist">
+        <echo>
+    ***** Properties of published ${plugin.jar} *****
+    Commit message    : '${commit.message}'                    
+    Plugin-Mainversion: ${plugin.main.version}
+    JOSM build version: ${coreversion.info.entry.revision}
+    Plugin-Version    : ${version.entry.commit.revision}
+    ***** / Properties of published ${plugin.jar} *****                    
+                        
+    Now commiting ${plugin.jar} ...
+    </echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="-m '${commit.message}'"/>
+            <arg value="commit"/>
+            <arg value="${plugin.jar}"/>
+        </exec>
+    </target>
+
+    <!-- ** make sure svn is present as a command line tool ** -->
+    <target name="ensure-svn-present">
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false" failonerror="false" resultproperty="svn.exit.code">
+            <env key="LANG" value="C" />
+            <arg value="--version" />
+        </exec>
+        <fail message="Fatal: command 'svn --version' failed. Please make sure svn is installed on your system.">
+            <!-- return code not set at all? Most likely svn isn't installed -->
+            <condition>
+                <not>
+                    <isset property="svn.exit.code" />
+                </not>
+            </condition>
+        </fail>
+        <fail message="Fatal: command 'svn --version' failed. Please make sure a working copy of svn is installed on your system.">
+            <!-- error code from SVN? Most likely svn is not what we are looking on this system -->
+            <condition>
+                <isfailure code="${svn.exit.code}" />
+            </condition>
+        </fail>
+    </target>
+
+    <target name="publish" depends="ensure-svn-present,core-info,commit-current,update-current,clean,dist,commit-dist">
+    </target>
+</project>
diff --git a/imagery/images/OLmarker.png b/imagery/images/OLmarker.png
new file mode 100644
index 0000000..b1917f2
Binary files /dev/null and b/imagery/images/OLmarker.png differ
diff --git a/imagery/images/bing_maps.png b/imagery/images/bing_maps.png
new file mode 100644
index 0000000..ae4367e
Binary files /dev/null and b/imagery/images/bing_maps.png differ
diff --git a/imagery/images/blankmenu.png b/imagery/images/blankmenu.png
new file mode 100644
index 0000000..640fbde
Binary files /dev/null and b/imagery/images/blankmenu.png differ
diff --git a/imagery/images/cursor/modifier/move.png b/imagery/images/cursor/modifier/move.png
new file mode 100644
index 0000000..54a0b84
Binary files /dev/null and b/imagery/images/cursor/modifier/move.png differ
diff --git a/imagery/images/imagery.png b/imagery/images/imagery.png
new file mode 100644
index 0000000..447fbf2
Binary files /dev/null and b/imagery/images/imagery.png differ
diff --git a/imagery/images/imagery_menu.png b/imagery/images/imagery_menu.png
new file mode 100644
index 0000000..a7a657b
Binary files /dev/null and b/imagery/images/imagery_menu.png differ
diff --git a/imagery/images/imagery_small.png b/imagery/images/imagery_small.png
new file mode 100644
index 0000000..770713c
Binary files /dev/null and b/imagery/images/imagery_small.png differ
diff --git a/imagery/images/load.png b/imagery/images/load.png
new file mode 100644
index 0000000..2019dc8
Binary files /dev/null and b/imagery/images/load.png differ
diff --git a/imagery/images/mapmode/adjustimg.png b/imagery/images/mapmode/adjustimg.png
new file mode 100644
index 0000000..b592bd0
Binary files /dev/null and b/imagery/images/mapmode/adjustimg.png differ
diff --git a/imagery/images/preferences/imagery.png b/imagery/images/preferences/imagery.png
new file mode 100644
index 0000000..115b1e5
Binary files /dev/null and b/imagery/images/preferences/imagery.png differ
diff --git a/imagery/images/save.png b/imagery/images/save.png
new file mode 100644
index 0000000..2949b3f
Binary files /dev/null and b/imagery/images/save.png differ
diff --git a/imagery/images_nodist/imagery.xcf b/imagery/images_nodist/imagery.xcf
new file mode 100644
index 0000000..7eab2db
Binary files /dev/null and b/imagery/images_nodist/imagery.xcf differ
diff --git a/imagery/sources.cfg b/imagery/sources.cfg
new file mode 100644
index 0000000..3fd66c2
--- /dev/null
+++ b/imagery/sources.cfg
@@ -0,0 +1,69 @@
+# OUTDATED - only for old plugins
+# See http://josm.openstreetmap.de/wiki/Maps for newer data.
+#
+# FORMAT
+# default(true or false);Name;URL
+# NOTE: default items should be common and worldwide
+#
+true;Landsat;wms:http://onearth.jpl.nasa.gov/wms.cgi?request=GetMap&layers=global_mosaic&styles=&format=image/jpeg&
+true;Landsat (mirror);wms:http://irs.gis-lab.info/?layers=landsat&
+false;Open Aerial Map;wms:http://openaerialmap.org/wms/?VERSION=1.0&request=GetMap&layers=world&styles=&format=image/jpeg&
+#
+# different forms of imagery
+true;Bing sat;bing:bing
+true;Yahoo Sat;html:http://josm.openstreetmap.de/wmsplugin/YahooDirect.html?
+true;OpenStreetMap;tms:http://tile.openstreetmap.org/
+false;OpenCycleMap;tms:http://tile.opencyclemap.org/cycle/
+false;TilesAtHome;tms:http://tah.openstreetmap.org/Tiles/tile/
+#
+#
+# only for Germany
+false;Streets NRW Geofabrik.de;wms:http://tools.geofabrik.de/osmi/view/strassennrw/josmwms?
+#
+#
+# only for North America
+# Terraserver USCG - High resolution maps
+false;Terraserver Topo;wms:http://terraservice.net/ogcmap.ashx?version=1.1.1&request=GetMap&Layers=drg&styles=&format=image/jpeg&
+false;Terraserver Urban;wms:http://terraservice.net/ogcmap.ashx?version=1.1.1&request=GetMap&Layers=urbanarea&styles=&format=image/jpeg&
+#
+#
+# only for Czech Republic
+false;Czech CUZK:KM;wms:http://wms.cuzk.cz/wms.asp?service=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:4326&LAYERS=parcelni_cisla_i,obrazy_parcel_i,RST_KMD_I,hranice_parcel_i,DEF_BUDOVY,RST_KN_I,dalsi_p_mapy_i,prehledka_kat_prac,prehledka_kat_uz,prehledka_kraju-linie&FORMAT=image/png&transparent=TRUE&
+false;Czech UHUL:ORTOFOTO;wms:http://geoportal2.uhul.cz/cgi-bin/oprl.asp?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:4326&LAYERS=Ortofoto_cb&STYLES=default&FORMAT=image/jpeg&TRANSPARENT=TRUE&
+#
+#
+# only for GB
+# fails with division by zero error
+false;NPE Maps;wms:http://nick.dev.openstreetmap.org/openpaths/freemap.php?layers=npe&
+false;NPE Maps (Tim);wms:http://dev.openstreetmap.org/~timsc/wms2/map.php?
+false;7th Series (OS7);wms:http://ooc.openstreetmap.org/wms/map.php?source=os7&
+#
+#
+# only for Japan
+false;MLIT Japan (ORTHO);wms:http://orthophoto.mlit.go.jp:8888/wms/service/wmsRasterTileMap?VERSION=1.3.0&REQUEST=GetMap&LAYERS=ORTHO&STYLES=Default&CRS=EPSG:4612&BBOX={s},{w},{n},{e}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png&BGCOLOR=OxFFFFFF
+false;MLIT Japan (ORTHO01);wms:http://orthophoto.mlit.go.jp:8888/wms/service/wmsRasterTileMap?VERSION=1.3.0&REQUEST=GetMap&LAYERS=ORTHO01&STYLES=Default&CRS=EPSG:4612&BBOX={s},{w},{n},{e}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png&BGCOLOR=OxFFFFFF
+false;MLIT Japan (ORTHO02);wms:http://orthophoto.mlit.go.jp:8888/wms/service/wmsRasterTileMap?VERSION=1.3.0&REQUEST=GetMap&LAYERS=ORTHO02&STYLES=Default&CRS=EPSG:4612&BBOX={s},{w},{n},{e}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png&BGCOLOR=OxFFFFFF
+false;MLIT Japan (ORTHO03);wms:http://orthophoto.mlit.go.jp:8888/wms/service/wmsRasterTileMap?VERSION=1.3.0&REQUEST=GetMap&LAYERS=ORTHO03&STYLES=Default&CRS=EPSG:4612&BBOX={s},{w},{n},{e}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png&BGCOLOR=OxFFFFFF
+#
+#
+# only for Italy
+false;Lodi - Italy;wms:http://sit.provincia.lodi.it/mapserver/mapserv.exe?map=ortofoto_wgs84.map&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:4326&LAYERS=Terraitaly%20Ortofoto%202007&STYLES=%2C%2C&FORMAT=image/png&TRANSPARENT=TRUE&
+false;Sicily - Italy;wms:http://88.53.214.52/sitr/services/WGS84_F33/Ortofoto_ATA20072008_f33/MapServer/WMSServer?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&CRS=CRS:84&LAYERS=0&STYLES=default&FORMAT=image/jpeg&
+false;PCN 2006 - Italy;wms:http://wms.pcn.minambiente.it/cgi-bin/mapserv.exe?map=/ms_ogc/service/ortofoto_colore_06.map&LAYERS=ortofoto_colore_06_32,ortofoto_colore_06_33&REQUEST=GetMap&VERSION=1.1.1&FORMAT=image%2Fjpeg&
+#
+# only for France
+false;SPOTMaps (France);wms:http://spotmaps.youmapps.org/cgi-bin/mapserv?map=/home/ortho/ortho.map&service=wms&version=1.1.1&srs=EPSG:4326&request=GetMap&layers=spotmaps4osm&format=image/jpeg&FORMAT=image/jpeg&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&Layers=demo&;http://www.youmapps.org/licenses/EULA-OSM-J-{lang}.html
+#
+#
+# URLS must be designed to append arguments directly behind. So the URLS should either end with '?' or '&'
+# Following arguments are added: width, height, bbox, srs (projection method)
+# srs is only added when no srs is given already (In this case the projection is checked
+# and an error is issued when they mismatch).
+#
+# If more specific URL design is needed, then patterns are supported as well. If
+# patterns are found no other arguments are added:
+# {proj} is replaced by projection
+# {bbox} is replaced by bounding box using projected coordinates
+# {width} is requested display width
+# {height} is requested display height
+# {w},{s},{n},{e} are replaced by corresponding coordinates
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/AddImageryLayerAction.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/AddImageryLayerAction.java
new file mode 100644
index 0000000..a78f56c
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/AddImageryLayerAction.java
@@ -0,0 +1,25 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+
+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);
+    }
+};
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryAdjustAction.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryAdjustAction.java
new file mode 100644
index 0000000..d6b2acf
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryAdjustAction.java
@@ -0,0 +1,207 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.GridBagLayout;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.util.List;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.Icon;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+
+public class ImageryAdjustAction extends MapMode implements MouseListener, MouseMotionListener{
+
+    boolean mouseDown;
+    EastNorth prevEastNorth;
+    private ImageryLayer adjustingLayer;
+
+    public ImageryAdjustAction(MapFrame mapFrame) {
+        super(tr("Adjust imagery"), "adjustimg",
+                tr("Adjust the position of the selected imagery layer"), mapFrame,
+                ImageProvider.getCursor("normal", "move"));
+    }
+
+    @Override public void enterMode() {
+        super.enterMode();
+        if (!hasLayersToAdjust()) {
+            warnNoImageryLayers();
+            return;
+        }
+        List<ImageryLayer> imageryLayers = Main.map.mapView.getLayersOfType(ImageryLayer.class);
+        if (imageryLayers.size() == 1) {
+            adjustingLayer = imageryLayers.get(0);
+        } else {
+            adjustingLayer = (ImageryLayer)askAdjustLayer(Main.map.mapView.getLayersOfType(ImageryLayer.class));
+        }
+        if (adjustingLayer == null)
+            return;
+        if (!adjustingLayer.isVisible()) {
+            adjustingLayer.setVisible(true);
+        }
+        Main.map.mapView.addMouseListener(this);
+        Main.map.mapView.addMouseMotionListener(this);
+    }
+
+    @Override public void exitMode() {
+        super.exitMode();
+        Main.map.mapView.removeMouseListener(this);
+        Main.map.mapView.removeMouseMotionListener(this);
+        adjustingLayer = null;
+    }
+
+    @Override public void mousePressed(MouseEvent e) {
+        if (e.getButton() != MouseEvent.BUTTON1)
+            return;
+
+        if (adjustingLayer.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 (adjustingLayer == null || prevEastNorth == null) return;
+        EastNorth eastNorth =
+            Main.map.mapView.getEastNorth(e.getX(),e.getY());
+        adjustingLayer.displace(
+                eastNorth.east()-prevEastNorth.east(),
+                eastNorth.north()-prevEastNorth.north()
+        );
+        prevEastNorth = eastNorth;
+        Main.map.mapView.repaint();
+    }
+
+    @Override public void mouseReleased(MouseEvent e) {
+        Main.map.mapView.repaint();
+        Main.map.mapView.setCursor(Cursor.getDefaultCursor());
+        prevEastNorth = null;
+    }
+
+    @Override
+    public void mouseEntered(MouseEvent e) {
+    }
+
+    @Override
+    public void mouseExited(MouseEvent e) {
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e) {
+    }
+
+    @Override public void mouseClicked(MouseEvent e) {
+    }
+
+    @Override public boolean layerIsSupported(Layer l) {
+        return hasLayersToAdjust();
+    }
+
+    /**
+     * the list cell renderer used to render layer list entries
+     *
+     */
+    static public class LayerListCellRenderer extends DefaultListCellRenderer {
+
+        protected boolean isActiveLayer(Layer layer) {
+            if (Main.map == null)
+                return false;
+            if (Main.map.mapView == null)
+                return false;
+            return Main.map.mapView.getActiveLayer() == layer;
+        }
+
+        @Override
+        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
+                boolean cellHasFocus) {
+            Layer layer = (Layer) value;
+            JLabel label = (JLabel) super.getListCellRendererComponent(list, layer.getName(), index, isSelected,
+                    cellHasFocus);
+            Icon icon = layer.getIcon();
+            label.setIcon(icon);
+            label.setToolTipText(layer.getToolTipText());
+            return label;
+        }
+    }
+
+    /**
+     * Prompts the user with a list of imagery layers which can be adjusted
+     *
+     * @param adjustableLayers the list of adjustable layers
+     * @return  the selected layer; null, if no layer was selected
+     */
+    protected Layer askAdjustLayer(List<? extends Layer> adjustableLayers) {
+        JComboBox layerList = new JComboBox();
+        layerList.setRenderer(new LayerListCellRenderer());
+        layerList.setModel(new DefaultComboBoxModel(adjustableLayers.toArray()));
+        layerList.setSelectedIndex(0);
+
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new GridBagLayout());
+        pnl.add(new JLabel(tr("Please select the imagery layer to adjust.")), GBC.eol());
+        pnl.add(layerList, GBC.eol());
+
+        ExtendedDialog diag = new ExtendedDialog(
+                Main.parent,
+                tr("Select imagery layer"),
+                new String[] { tr("Start adjusting"),tr("Cancel") }
+        );
+        diag.setContent(pnl);
+        diag.setButtonIcons(new String[] { "mapmode/adjustimg", "cancel" });
+        diag.showDialog();
+        int decision = diag.getValue();
+        if (decision != 1)
+            return null;
+        Layer adjustLayer = (Layer) layerList.getSelectedItem();
+        return adjustLayer;
+    }
+
+    /**
+     * Displays a warning message if there are no imagery layers to adjust
+     *
+     */
+    protected void warnNoImageryLayers() {
+        JOptionPane.showMessageDialog(
+                Main.parent,
+                tr("There are currently no imagery layer to adjust."),
+                tr("No layers to adjust"),
+                JOptionPane.WARNING_MESSAGE
+        );
+    }
+
+    /**
+     * Replies true if there is at least one WMS layer
+     *
+     * @return true if there is at least one WMS layer
+     */
+    protected boolean hasLayersToAdjust() {
+        if (Main.map == null) return false;
+        if (Main.map.mapView == null) return false;
+        return ! Main.map.mapView.getLayersOfType(ImageryLayer.class).isEmpty();
+    }
+
+    @Override
+    protected void updateEnabledState() {
+        setEnabled(hasLayersToAdjust());
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryInfo.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryInfo.java
new file mode 100644
index 0000000..88c50b7
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryInfo.java
@@ -0,0 +1,185 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Class that stores info about a WMS server.
+ *
+ * @author Frederik Ramm <frederik at 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;
+    String eulaAcceptanceRequired = null;
+    ImageryType imageryType = ImageryType.WMS;
+    double pixelPerDegree = 0.0;
+    int maxZoom = 0;
+
+    public ImageryInfo(String name) {
+        this.name=name;
+    }
+
+    public ImageryInfo(String name, String url) {
+        this.name=name;
+        setURL(url);
+    }
+
+    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;
+    }
+
+    public ImageryInfo(String name, String url, String cookies, double pixelPerDegree) {
+        this.name=name;
+        setURL(url);
+        this.cookies=cookies;
+        this.pixelPerDegree=pixelPerDegree;
+    }
+
+    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) {
+            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(imageryType == ImageryType.WMS && array.size() >= 4) this.pixelPerDegree=Double.valueOf(array.get(3));
+        if(imageryType == ImageryType.TMS && array.size() >= 4) this.maxZoom=Integer.valueOf(array.get(3));
+    }
+
+    public ImageryInfo(ImageryInfo i) {
+        this.name=i.name;
+        this.url=i.url;
+        this.cookies=i.cookies;
+        this.imageryType=i.imageryType;
+        this.pixelPerDegree=i.pixelPerDegree;
+    }
+
+    @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 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;
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayer.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayer.java
new file mode 100644
index 0000000..3187256
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayer.java
@@ -0,0 +1,81 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import java.awt.Toolkit;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.plugins.imagery.tms.TMSLayer;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSLayer;
+
+public abstract class ImageryLayer extends Layer {
+    protected static final Icon icon =
+        new ImageIcon(Toolkit.getDefaultToolkit().createImage(ImageryPlugin.class.getResource("/images/imagery_small.png")));
+
+    protected ImageryInfo info;
+    protected MapView mv;
+
+    protected double dx = 0.0;
+    protected double dy = 0.0;
+
+    public ImageryLayer(ImageryInfo info) {
+        super(info.getName());
+        this.info = info;
+        this.mv = Main.map.mapView;
+    }
+
+
+    public double getPPD(){
+        ProjectionBounds bounds = mv.getProjectionBounds();
+        return mv.getWidth() / (bounds.max.east() - bounds.min.east());
+    }
+
+    public void displace(double dx, double dy) {
+        this.dx += dx;
+        this.dy += dy;
+    }
+
+    public double getDx() {
+        return dx;
+    }
+
+    public double getDy() {
+        return 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.imageryType == ImageryType.WMS || info.imageryType == ImageryType.HTML) {
+            return new WMSLayer(info);
+        } else if (info.imageryType == ImageryType.TMS || info.imageryType == ImageryType.BING) {
+            return new TMSLayer(info);
+        } else throw new AssertionError();
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayerInfo.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayerInfo.java
new file mode 100644
index 0000000..a648566
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayerInfo.java
@@ -0,0 +1,104 @@
+package org.openstreetmap.josm.plugins.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 org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.io.MirroredInputStream;
+
+public class 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, ImageryPlugin.instance.getPluginDir(), -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);
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java
new file mode 100644
index 0000000..393a8be
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java
@@ -0,0 +1,273 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.IconToggleButton;
+import org.openstreetmap.josm.gui.MainMenu;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginHandler;
+import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.plugins.PluginProxy;
+import org.openstreetmap.josm.plugins.imagery.wms.Map_Rectifier_WMSmenuAction;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSAdapter;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSLayer;
+import org.openstreetmap.josm.plugins.imagery.wms.io.WMSLayerExporter;
+import org.openstreetmap.josm.plugins.imagery.wms.io.WMSLayerImporter;
+
+public class ImageryPlugin extends Plugin {
+
+    JMenu imageryJMenu;
+
+    public static ImageryPlugin instance;
+    public static WMSAdapter wmsAdapter = new WMSAdapter();
+
+    public ImageryLayerInfo info = new ImageryLayerInfo();
+
+    // remember state of menu item to restore on changed preferences
+    private boolean menuEnabled = false;
+
+    /***************************************************************
+     * Remote control initialization:
+     * If you need remote control in some other plug-in
+     * copy this stuff and the call to initRemoteControl below
+     * and replace the RequestHandler subclass in initRemoteControl
+     ***************************************************************/
+
+    /** name of remote control plugin */
+    private final String REMOTECONTROL_NAME = "remotecontrol";
+
+    /* if necessary change these version numbers to ensure compatibility */
+
+    /** RemoteControlPlugin older than this SVN revision is not compatible */
+    final int REMOTECONTROL_MIN_REVISION = 22734;
+    /** WMSPlugin needs this specific API major version of RemoteControlPlugin */
+    final int REMOTECONTROL_NEED_API_MAJOR = 1;
+    /** All API minor versions starting from this should be compatible */
+    final int REMOTECONTROL_MIN_API_MINOR = 0;
+
+    /* these fields will contain state and version of remote control plug-in */
+    boolean remoteControlAvailable = false;
+    boolean remoteControlCompatible = true;
+    boolean remoteControlInitialized = false;
+    int remoteControlRevision = 0;
+    int remoteControlApiMajor = 0;
+    int remoteControlApiMinor = 0;
+    int remoteControlProtocolMajor = 0;
+    int remoteControlProtocolMinor = 0;
+
+    /**
+     * Check if remote control plug-in is available and if its version is
+     * high enough and register remote control command for this plug-in.
+     */
+    private void initRemoteControl() {
+        for(PluginProxy pp: PluginHandler.pluginList)
+        {
+            PluginInformation info = pp.getPluginInformation();
+            if(REMOTECONTROL_NAME.equals(info.name))
+            {
+                remoteControlAvailable = true;
+                remoteControlRevision = Integer.parseInt(info.version);
+                if(REMOTECONTROL_MIN_REVISION > remoteControlRevision)
+                {
+                    remoteControlCompatible = false;
+                }
+            }
+        }
+
+        if(remoteControlAvailable && remoteControlCompatible)
+        {
+            Plugin plugin =
+                (Plugin) PluginHandler.getPlugin(REMOTECONTROL_NAME);
+            try {
+                Method method;
+                method = plugin.getClass().getMethod("getVersion");
+                Object obj = method.invoke(plugin);
+                if((obj != null ) && (obj instanceof int[]))
+                {
+                    int[] versions = (int[]) obj;
+                    if(versions.length >= 4)
+                    {
+                        remoteControlApiMajor = versions[0];
+                        remoteControlApiMinor = versions[1];
+                        remoteControlProtocolMajor = versions[2];
+                        remoteControlProtocolMinor = versions[3];
+                    }
+                }
+
+                if((remoteControlApiMajor != REMOTECONTROL_NEED_API_MAJOR) ||
+                        (remoteControlApiMinor < REMOTECONTROL_MIN_API_MINOR))
+                {
+                    remoteControlCompatible = false;
+                }
+                if(remoteControlCompatible)
+                {
+                    System.out.println(this.getClass().getSimpleName() + ": initializing remote control");
+                    method = plugin.getClass().getMethod("addRequestHandler", String.class, Class.class);
+                    // replace command and class when you copy this to some other plug-in
+                    // for compatibility with old remotecontrol add leading "/"
+                    method.invoke(plugin, "/" + ImageryRemoteHandler.command, ImageryRemoteHandler.class);
+                    remoteControlInitialized = true;
+                }
+            } catch (SecurityException e) {
+                e.printStackTrace();
+            } catch (NoSuchMethodException e) {
+                e.printStackTrace();
+            } catch (IllegalArgumentException e) {
+                e.printStackTrace();
+            } catch (IllegalAccessException e) {
+                e.printStackTrace();
+            } catch (InvocationTargetException e) {
+                e.printStackTrace();
+            }
+        }
+        if(remoteControlAvailable)
+        {
+            String msg = null;
+
+            if(remoteControlCompatible)
+            {
+                if(!remoteControlInitialized)
+                {
+                    msg  = tr("Could not initialize remote control.");
+                }
+            }
+            else
+            {
+                msg  = tr("Remote control plugin is not compatible with {0}.",
+                        this.getClass().getSimpleName());
+            }
+
+            if(msg != null)
+            {
+                String additionalMessage = tr("{0} will work but remote control for this plugin is disabled.\n"
+                        + "You should update the plugins.",
+                        this.getClass().getSimpleName());
+                String versionMessage = tr("Current version of \"{1}\": {2}, internal version {3}. "
+                        + "Need version {4}, internal version {5}.\n"
+                        + "If updating the plugins does not help report a bug for \"{0}\".",
+                        this.getClass().getSimpleName(),
+                        REMOTECONTROL_NAME,
+                        ""+remoteControlRevision,
+                        (remoteControlApiMajor != 0) ?
+                                ""+remoteControlApiMajor+"."+remoteControlApiMinor :
+                                    tr("unknown"),
+                                    ""+REMOTECONTROL_MIN_REVISION,
+                                    ""+REMOTECONTROL_NEED_API_MAJOR+"."+REMOTECONTROL_MIN_API_MINOR );
+
+                String title = tr("{0}: Problem with remote control",
+                        this.getClass().getSimpleName());
+
+                System.out.println(this.getClass().getSimpleName() + ": " +
+                        msg + "\n" + versionMessage);
+
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        msg + "\n" + additionalMessage,
+                        title,
+                        JOptionPane.WARNING_MESSAGE
+                );
+            }
+        }
+
+        if(!remoteControlAvailable) {
+            System.out.println(this.getClass().getSimpleName() + ": remote control not available");
+        }
+    }
+
+    /***************************************
+     * end of remote control initialization
+     ***************************************/
+
+    protected void initExporterAndImporter() {
+        ExtensionFileFilter.exporters.add(new WMSLayerExporter());
+        ExtensionFileFilter.importers.add(new WMSLayerImporter());
+    }
+
+    public ImageryPlugin(PluginInformation info) {
+        super(info);
+        instance = this;
+        this.info.load();
+        refreshMenu();
+        initRemoteControl();
+    }
+
+    public void addLayer(ImageryInfo info) {
+        this.info.add(info);
+        this.info.save();
+        refreshMenu();
+    }
+
+    public void refreshMenu() {
+        MainMenu menu = Main.main.menu;
+
+        if (imageryJMenu == null)
+            imageryJMenu = menu.addMenu(marktr("Imagery"), KeyEvent.VK_W, menu.defaultMenuPos, ht("/Plugin/Imagery"));
+        else
+            imageryJMenu.removeAll();
+
+        // for each configured WMSInfo, add a menu entry.
+        for (final ImageryInfo u : info.layers) {
+            imageryJMenu.add(new JMenuItem(new AddImageryLayerAction(u)));
+        }
+        imageryJMenu.addSeparator();
+        imageryJMenu.add(new JMenuItem(new Map_Rectifier_WMSmenuAction()));
+
+        imageryJMenu.addSeparator();
+        imageryJMenu.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());
+            }
+        }));
+        setEnabledAll(menuEnabled);
+    }
+
+    private void setEnabledAll(boolean isEnabled) {
+        for(int i=0; i < imageryJMenu.getItemCount(); i++) {
+            JMenuItem item = imageryJMenu.getItem(i);
+
+            if(item != null) item.setEnabled(isEnabled);
+        }
+        menuEnabled = isEnabled;
+    }
+
+    @Override
+    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+        if (oldFrame==null && newFrame!=null) {
+            setEnabledAll(true);
+            Main.map.addMapMode(new IconToggleButton
+                    (new ImageryAdjustAction(Main.map)));
+        } else if (oldFrame!=null && newFrame==null ) {
+            setEnabledAll(false);
+        }
+    }
+
+    @Override
+    public PreferenceSetting getPreferenceSetting() {
+        return new ImageryPreferenceEditor();
+    }
+
+    @Override
+    public String getPluginDir()
+    {
+        return new File(Main.pref.getPluginsDirectory(), "imagery").getPath();
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferenceEditor.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferenceEditor.java
new file mode 100644
index 0000000..92e1a43
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferenceEditor.java
@@ -0,0 +1,540 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+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.Locale;
+
+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.gui.preferences.PreferenceSetting;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.plugins.imagery.tms.TMSPreferences;
+import org.openstreetmap.josm.plugins.imagery.wms.AddWMSLayerPanel;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSAdapter;
+import org.openstreetmap.josm.tools.ColorHelper;
+import org.openstreetmap.josm.tools.GBC;
+
+public class ImageryPreferenceEditor implements PreferenceSetting {
+    private ImageryLayerTableModel model;
+    private JComboBox browser;
+
+    // Common settings
+    private Color colFadeColor;
+    private JButton btnFadeColor;
+    private JSlider fadeAmount = new JSlider(0, 100);
+    private JCheckBox remoteCheckBox;
+    boolean allowRemoteControl = true;
+
+    // WMS Settings
+    JCheckBox overlapCheckBox;
+    JSpinner spinEast;
+    JSpinner spinNorth;
+    JSpinner spinSimConn;
+    WMSAdapter wmsAdapter = ImageryPlugin.wmsAdapter;
+    ImageryPlugin plugin = ImageryPlugin.instance;
+
+    //TMS settings controls
+    private JCheckBox autozoomActive = new JCheckBox();
+    private JCheckBox autoloadTiles = new JCheckBox();
+    private JSpinner minZoomLvl;
+    private JSpinner maxZoomLvl;
+
+
+    private JPanel buildImageryProvidersPanel(final PreferenceTabbedPane gui) {
+        final JPanel p = new JPanel(new GridBagLayout());
+        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);
+        p.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 (!confirmeEulaAcceptance(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));
+                }
+            }
+        });
+
+        p.add(buttonPanel);
+        p.add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+        // Add default item list
+        p.add(scrolldef, GBC.eol().insets(0, 5, 0, 0).fill(GridBagConstraints.BOTH));
+
+        return p;
+    }
+
+    private JPanel buildCommonSettingsPanel(final PreferenceTabbedPane gui) {
+        final JPanel p = new JPanel(new GridBagLayout());
+
+        this.colFadeColor = ImageryPreferences.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(ImageryPreferences.PROP_FADE_AMOUNT.get());
+
+        allowRemoteControl = ImageryPreferences.PROP_REMOTE_CONTROL.get();
+        remoteCheckBox = new JCheckBox(tr("Allow remote control (reqires remotecontrol plugin)"), allowRemoteControl);
+        p.add(remoteCheckBox,GBC.eol().fill(GBC.HORIZONTAL));
+
+        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(Main.pref.get("wmsplugin.browser", "webkit-image {0}"));
+        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"), wmsAdapter.PROP_OVERLAP.get());
+        JLabel labelEast = new JLabel(tr("% of east:"));
+        JLabel labelNorth = new JLabel(tr("% of north:"));
+        spinEast = new JSpinner(new SpinnerNumberModel(wmsAdapter.PROP_OVERLAP_EAST.get(), 1, 50, 1));
+        spinNorth = new JSpinner(new SpinnerNumberModel(wmsAdapter.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(wmsAdapter.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(TMSPreferences.DEFAULT_MIN_ZOOM, TMSPreferences.MIN_ZOOM, TMSPreferences.MAX_ZOOM, 1));
+        maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSPreferences.DEFAULT_MAX_ZOOM, TMSPreferences.MIN_ZOOM, TMSPreferences.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(Box.createVerticalGlue(), GBC.eol().fill(GBC.VERTICAL));
+
+        this.autozoomActive.setSelected(TMSPreferences.PROP_DEFAULT_AUTOZOOM.get());
+        this.autoloadTiles.setSelected(TMSPreferences.PROP_DEFAULT_AUTOLOAD.get());
+        this.maxZoomLvl.setValue(TMSPreferences.getMaxZoomLvl(null));
+        this.minZoomLvl.setValue(TMSPreferences.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();
+        pane.add(buildImageryProvidersPanel(gui));
+        pane.add(buildSettingsPanel(gui));
+        pane.setTitleAt(0, tr("Imagery providers"));
+        pane.setTitleAt(1, tr("Settings"));
+        p.add(pane,GBC.std().fill(GBC.BOTH));
+    }
+
+    @Override
+    public boolean ok() {
+        plugin.info.save();
+        plugin.refreshMenu();
+
+        wmsAdapter.PROP_OVERLAP.put(overlapCheckBox.getModel().isSelected());
+        wmsAdapter.PROP_OVERLAP_EAST.put((Integer) spinEast.getModel().getValue());
+        wmsAdapter.PROP_OVERLAP_NORTH.put((Integer) spinNorth.getModel().getValue());
+        wmsAdapter.PROP_SIMULTANEOUS_CONNECTIONS.put((Integer) spinSimConn.getModel().getValue());
+        allowRemoteControl = remoteCheckBox.getModel().isSelected();
+
+        Main.pref.put("wmsplugin.browser", browser.getEditor().getItem().toString());
+
+
+        TMSPreferences.PROP_DEFAULT_AUTOZOOM.put(this.autozoomActive.isSelected());
+        TMSPreferences.PROP_DEFAULT_AUTOLOAD.put(this.autoloadTiles.isSelected());
+        TMSPreferences.setMaxZoomLvl((Integer)this.maxZoomLvl.getValue());
+        TMSPreferences.setMinZoomLvl((Integer)this.minZoomLvl.getValue());
+
+        ImageryPreferences.PROP_REMOTE_CONTROL.put(allowRemoteControl);
+        ImageryPreferences.PROP_FADE_AMOUNT.put(this.fadeAmount.getValue());
+        ImageryPreferences.setFadeColor(this.colFadeColor);
+
+        return false;
+    }
+
+    /**
+     * Updates a server URL in the preferences dialog. Used by other plugins.
+     *
+     * @param server
+     *            The server name
+     * @param url
+     *            The server URL
+     */
+    public void setServerUrl(String server, String url) {
+        for (int i = 0; i < model.getRowCount(); i++) {
+            if (server.equals(model.getValueAt(i, 0).toString())) {
+                model.setValueAt(url, i, 1);
+                return;
+            }
+        }
+        model.addRow(new String[] { server, url });
+    }
+
+    /**
+     * Gets a server URL in the preferences dialog. Used by other plugins.
+     *
+     * @param server
+     *            The server name
+     * @return The server URL
+     */
+    public String getServerUrl(String server) {
+        for (int i = 0; i < model.getRowCount(); i++) {
+            if (server.equals(model.getValueAt(i, 0).toString())) {
+                return model.getValueAt(i, 1).toString();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * The table model for the WMS layer
+     *
+     */
+    class ImageryLayerTableModel extends DefaultTableModel {
+        public ImageryLayerTableModel() {
+            setColumnIdentifiers(new String[] { tr("Menu Name"), tr("Imagery URL"), trc("layer", "Zoom") });
+        }
+
+        public ImageryInfo getRow(int row) {
+            return plugin.info.layers.get(row);
+        }
+
+        public void addRow(ImageryInfo i) {
+            plugin.info.add(i);
+            int p = getRowCount() - 1;
+            fireTableRowsInserted(p, p);
+        }
+
+        @Override
+        public void removeRow(int i) {
+            plugin.info.remove(getRow(i));
+            fireTableRowsDeleted(i, i);
+        }
+
+        @Override
+        public int getRowCount() {
+            return plugin.info.layers.size();
+        }
+
+        @Override
+        public Object getValueAt(int row, int column) {
+            ImageryInfo info = plugin.info.layers.get(row);
+            switch (column) {
+            case 0:
+                return info.name;
+            case 1:
+                return info.getFullURL();
+            case 2:
+                return (info.imageryType == ImageryType.WMS) ? (info.pixelPerDegree == 0.0 ? "" : info.pixelPerDegree)
+                                                             : (info.maxZoom == 0 ? "" : info.maxZoom);
+            }
+            return null;
+        }
+
+        @Override
+        public void setValueAt(Object o, int row, int column) {
+            ImageryInfo info = plugin.info.layers.get(row);
+            switch (column) {
+            case 0:
+                info.name = (String) o;
+            case 1:
+                info.setURL((String)o);
+            case 2:
+                info.pixelPerDegree = 0;
+                info.maxZoom = 0;
+                try {
+                    if(info.imageryType == ImageryType.WMS)
+                        info.pixelPerDegree = Double.parseDouble((String) o);
+                    else
+                        info.maxZoom = Integer.parseInt((String) o);
+                } catch (NumberFormatException e) {
+                }
+            }
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return true;
+        }
+    }
+
+    /**
+     * The table model for the WMS layer
+     *
+     */
+    class ImageryDefaultLayerTableModel extends DefaultTableModel {
+        public ImageryDefaultLayerTableModel() {
+            setColumnIdentifiers(new String[] { tr("Menu Name (Default)"), tr("Imagery URL (Default)") });
+        }
+
+        public ImageryInfo getRow(int row) {
+            return plugin.info.defaultLayers.get(row);
+        }
+
+        @Override
+        public int getRowCount() {
+            return plugin.info.defaultLayers.size();
+        }
+
+        @Override
+        public Object getValueAt(int row, int column) {
+            ImageryInfo info = plugin.info.defaultLayers.get(row);
+            switch (column) {
+            case 0:
+                return info.name;
+            case 1:
+                return info.getFullURL();
+            }
+            return null;
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return false;
+        }
+    }
+
+    private boolean confirmeEulaAcceptance(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;
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferences.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferences.java
new file mode 100644
index 0000000..c55505d
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferences.java
@@ -0,0 +1,28 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import java.awt.Color;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+
+public class ImageryPreferences {
+
+    public static final BooleanProperty PROP_REMOTE_CONTROL = new BooleanProperty("imagery.remotecontrol", true);
+    public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 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);
+    }
+
+
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryRemoteHandler.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryRemoteHandler.java
new file mode 100644
index 0000000..8e2ee05
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryRemoteHandler.java
@@ -0,0 +1,108 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+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.plugins.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.plugins.remotecontrol.RequestHandler;
+import org.openstreetmap.josm.plugins.remotecontrol.RequestHandlerErrorException;
+
+public class ImageryRemoteHandler extends RequestHandler {
+
+    public static final String command = "imagery";
+
+    @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
+    public PermissionPrefWithDefault getPermissionPref()
+    {
+        return new PermissionPrefWithDefault(
+                "imagery.remotecontrol",
+                true,
+        "RemoteControl: Imagery forbidden by preferences");
+    }
+
+    @Override
+    protected String[] getMandatoryParams()
+    {
+        return new String[] { "url" };
+    }
+
+    @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;
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/BingAerialTileSource.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/BingAerialTileSource.java
new file mode 100644
index 0000000..c98f354
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/BingAerialTileSource.java
@@ -0,0 +1,191 @@
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+import java.awt.Image;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.gui.jmapviewer.OsmTileSource;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+public class BingAerialTileSource extends OsmTileSource.AbstractOsmTileSource {
+    private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
+    private static List<Attribution> attributions;
+
+    public BingAerialTileSource() {
+        super("Bing Aerial Maps", "http://ecn.t2.tiles.virtualearth.net/tiles/");
+
+        attributions = loadAttributionText();
+        System.err.println("Added " + attributions.size() + " attributions.");
+    }
+
+    class Attribution {
+        String attribution;
+        int minZoom;
+        int maxZoom;
+        Bounds bounds;
+    }
+
+    class AttrHandler extends DefaultHandler {
+
+        private String string;
+        private Attribution curr;
+        private List<Attribution> attributions = new ArrayList<Attribution>();
+        private double southLat;
+        private double northLat;
+        private double eastLon;
+        private double westLon;
+        private boolean inCoverage = false;
+
+        @Override
+        public void startElement(String uri, String stripped, String tagName,
+                Attributes attrs) throws SAXException {
+            if("ImageryProvider".equals(tagName)) {
+                curr = new Attribution();
+            } else if("CoverageArea".equals(tagName)) {
+                inCoverage = true;
+            }
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length)
+                throws SAXException {
+            string = new String(ch, start, length);
+        }
+
+        @Override
+        public void endElement(String uri, String stripped, String tagName)
+                throws SAXException {
+            if("ImageryProvider".equals(tagName)) {
+                attributions.add(curr);
+            } else if("Attribution".equals(tagName)) {
+                curr.attribution = string;
+            } else if(inCoverage && "ZoomMin".equals(tagName)) {
+                curr.minZoom = Integer.parseInt(string);
+            } else if(inCoverage && "ZoomMax".equals(tagName)) {
+                curr.maxZoom = Integer.parseInt(string);
+            } else if(inCoverage && "SouthLatitude".equals(tagName)) {
+                southLat = Double.parseDouble(string);
+            } else if(inCoverage && "NorthLatitude".equals(tagName)) {
+                northLat = Double.parseDouble(string);
+            } else if(inCoverage && "EastLongitude".equals(tagName)) {
+                eastLon = Double.parseDouble(string);
+            } else if(inCoverage && "WestLongitude".equals(tagName)) {
+                westLon = Double.parseDouble(string);
+            } else if("BoundingBox".equals(tagName)) {
+                curr.bounds = new Bounds(northLat, westLon, southLat, eastLon);
+            } else if("CoverageArea".equals(tagName)) {
+                inCoverage = false;
+            }
+            string = "";
+        }
+    }
+
+    private List<Attribution> loadAttributionText() {
+        try {
+            URL u = new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/0,0?zl=1&mapVersion=v1&key="+API_KEY+"&include=ImageryProviders&output=xml");
+            InputStream stream = u.openStream();
+            XMLReader parser = XMLReaderFactory.createXMLReader();
+            AttrHandler handler = new AttrHandler();
+            parser.setContentHandler(handler);
+            parser.parse(new InputSource(stream));
+            return handler.attributions;
+        } catch (IOException e) {
+            System.err.println("Could not open Bing aerials attribution metadata.");
+        } catch (SAXException e) {
+            System.err.println("Could not parse Bing aerials attribution metadata.");
+            e.printStackTrace();
+        }
+        return Collections.emptyList();
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return 22;
+    }
+
+    @Override
+    public String getExtension() {
+        return("jpeg");
+    }
+
+    @Override
+    public String getTilePath(int zoom, int tilex, int tiley) {
+        String quadtree = computeQuadTree(zoom, tilex, tiley);
+        return "/tiles/a" + quadtree + "." + getExtension() + "?g=587";
+    }
+
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+
+    @Override
+    public boolean requiresAttribution() {
+        return true;
+    }
+
+    @Override
+    public Image getAttributionImage() {
+        return ImageProvider.get("bing_maps").getImage();
+    }
+
+    @Override
+    public String getAttributionLinkURL() {
+        //return "http://bing.com/maps"
+        // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU
+        // (the requirement is that we have such a link at the bottom of the window)
+        return "http://go.microsoft.com/?linkid=9710837";
+    }
+
+    @Override
+    public String getTermsOfUseURL() {
+        return "http://opengeodata.org/microsoft-imagery-details";
+    }
+
+    @Override
+    public String getAttributionText(int zoom, LatLon topLeft, LatLon botRight) {
+        Bounds windowBounds = new Bounds(topLeft, botRight);
+        StringBuilder a = new StringBuilder();
+        for (Attribution attr : attributions) {
+            Bounds attrBounds = attr.bounds;
+            if(zoom <= attr.maxZoom && zoom >= attr.minZoom) {
+                if(windowBounds.getMin().lon() < attrBounds.getMax().lon()
+                        && windowBounds.getMax().lon() > attrBounds.getMin().lon()
+                        && windowBounds.getMax().lat() < attrBounds.getMin().lat()
+                        && windowBounds.getMin().lat() > attrBounds.getMax().lat()) {
+                    a.append(attr.attribution);
+                    a.append(" ");
+                }
+            }
+        }
+        return a.toString();
+    }
+
+    static String computeQuadTree(int zoom, int tilex, int tiley) {
+        StringBuilder k = new StringBuilder();
+        for(int i = zoom; i > 0; i--) {
+            char digit = 48;
+            int mask = 1 << (i - 1);
+            if ((tilex & mask) != 0) {
+                digit += 1;
+            }
+            if ((tiley & mask) != 0) {
+                digit += 2;
+            }
+            k.append(digit);
+        }
+        return k.toString();
+    }
+}
\ No newline at end of file
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSKey.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSKey.java
new file mode 100644
index 0000000..7cb2b8e
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSKey.java
@@ -0,0 +1,78 @@
+/**
+ *
+ */
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+/**
+ * <p>
+ * Key for map tile. Key have just X and Y value. It have overriden {@link #hashCode()},
+ * {@link #equals(Object)} and also {@link #toString()}.
+ * </p>
+ *
+ * @author LuVar <lubomir.varga at freemap.sk>
+ * @author Dave Hansen <dave at sr71.net>
+ *
+ */
+public class TMSKey {
+    private final int x;
+    private final int y;
+    private final int level;
+
+    /**
+     * <p>
+     * Constructs key for hashmaps for some tile describedy by X and Y position. X and Y are tiles
+     * positions on discrete map.
+     * </p>
+     *
+     * @param x x position in tiles table
+     * @param y y position in tiles table
+     */
+    public final boolean valid;
+    public TMSKey(int x, int y, int level) {
+        this.x = x;
+        this.y = y;
+        this.level = level;
+        if (level <= 0 || x < 0 || y < 0) {
+            this.valid = false;
+            System.err.println("invalid TMSKey("+level+", "+x+", "+y+")");
+        } else {
+            this.valid = true;
+        }
+    }
+
+    /**
+     * <p>
+     * Returns true ONLY if x and y are equals.
+     * </p>
+     *
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof TMSKey) {
+            TMSKey smk = (TMSKey) obj;
+            if((smk.x == this.x) && (smk.y == this.y) && (smk.level == this.level)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return  return new Integer(this.x + this.y * 10000).hashCode();
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return new Integer(this.x + this.y * 10000 + this.level * 100000).hashCode();
+    }
+
+    /**
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return "TMSKey(x=" + this.x + ",y=" + this.y + ",level=" + level + ")";
+    }
+
+}
diff --git a/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapLayer.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSLayer.java
similarity index 76%
copy from slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapLayer.java
copy to imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSLayer.java
index 1a1fb83..173a8da 100644
--- a/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapLayer.java
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSLayer.java
@@ -1,8 +1,9 @@
-package org.openstreetmap.josm.plugins.slippymap;
+package org.openstreetmap.josm.plugins.imagery.tms;
 
 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;
@@ -12,15 +13,20 @@ 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.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.Icon;
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JMenuItem;
 import javax.swing.JPopupMenu;
@@ -37,6 +43,7 @@ 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.osm.visitor.BoundingXYVisitor;
 import org.openstreetmap.josm.gui.MapView;
@@ -44,7 +51,10 @@ import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
 import org.openstreetmap.josm.gui.layer.Layer;
-import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo;
+import org.openstreetmap.josm.plugins.imagery.ImageryLayer;
+import org.openstreetmap.josm.plugins.imagery.ImageryPreferences;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
 
 /**
  * Class that displays a slippy map layer.
@@ -54,8 +64,7 @@ import org.openstreetmap.josm.tools.ImageProvider;
  * @author Dave Hansen <dave at sr71.net>
  *
  */
-public class SlippyMapLayer extends Layer implements ImageObserver,
-    TileLoaderListener {
+public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener {
     boolean debug = false;
     void out(String s)
     {
@@ -68,6 +77,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
     JobDispatcher jobDispatcher = JobDispatcher.getInstance();
 
     HashSet<Tile> tileRequestsOutstanding = new HashSet<Tile>();
+    @Override
     public synchronized void tileLoadingFinished(Tile tile, boolean success)
     {
         tile.setLoaded(true);
@@ -77,6 +87,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         if (debug)
             out("tileLoadingFinished() tile: " + tile + " success: " + success);
     }
+    @Override
     public TileCache getTileCache()
     {
         return tileCache;
@@ -91,18 +102,31 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
 
     /**
      * Actual zoom lvl. Initial zoom lvl is set to
-     * {@link SlippyMapPreferences#getMinZoomLvl()}.
      */
     public int currentZoomLevel;
 
-    LatLon lastTopLeft;
-    LatLon lastBotRight;
+    EastNorth lastTopLeft;
+    EastNorth lastBotRight;
     private Image bufferImage;
     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()
     {
@@ -110,50 +134,90 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         Main.map.repaint();
     }
 
-    void newTileStorage()
+    private void initTileSource(TileSource tileSource)
     {
-        int origZoom = currentZoomLevel;
-        tileSource = SlippyMapPreferences.getMapSource();
-        // The minimum should also take care of integer parsing
-        // errors which would leave us with a zoom of -1 otherwise
-        if (tileSource.getMaxZoom() < currentZoomLevel)
-            currentZoomLevel = tileSource.getMaxZoom();
-        if (tileSource.getMinZoom() > currentZoomLevel)
-            currentZoomLevel = tileSource.getMinZoom();
-        if (currentZoomLevel != origZoom) {
-            out("changed currentZoomLevel loading new tile store from " + origZoom + " to " + currentZoomLevel);
-            out("tileSource.getMinZoom(): " + tileSource.getMinZoom());
-            out("tileSource.getMaxZoom(): " + tileSource.getMaxZoom());
-            SlippyMapPreferences.setLastZoom(currentZoomLevel);
+        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 OsmTileLoader(this);
         tileLoader = new OsmFileCacheTileLoader(this);
     }
 
+    @Override
+    public void displace(double dx, double dy) {
+        super.displace(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 SlippyMapLayer() {
-        super(tr("Slippy Map"));
+    public TMSLayer(ImageryInfo info) {
+        super(info);
 
         setBackgroundLayer(true);
         this.setVisible(true);
 
-        currentZoomLevel = SlippyMapPreferences.getLastZoom();
-        newTileStorage();
+        if (info.getImageryType() == ImageryType.TMS) {
+            if(isUrlWithPatterns(info.getURL())) {
+                initTileSource(new TemplatedTMSTileSource(info.getName(), info.getURL(), info.getMaxZoom()));
+            } else {
+                initTileSource(new TMSTileSource(info.getName(),info.getURL(), info.getMaxZoom()));
+            }
+        } else if (info.getImageryType() == ImageryType.BING) {
+            initTileSource(new BingAerialTileSource());
+        } else throw new IllegalStateException("cannot create TMSLayer with non-TMS ImageryInfo");
 
         tileOptionMenu = new JPopupMenu();
 
+        autoZoom = TMSPreferences.PROP_DEFAULT_AUTOZOOM.get();
         autoZoomPopup = new JCheckBoxMenuItem();
         autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
+            @Override
             public void actionPerformed(ActionEvent ae) {
-                boolean new_state = !SlippyMapPreferences.getAutozoom();
-                SlippyMapPreferences.setAutozoom(new_state);
+                autoZoom = !autoZoom;
             }
         });
-        autoZoomPopup.setSelected(SlippyMapPreferences.getAutozoom());
+        autoZoomPopup.setSelected(autoZoom);
         tileOptionMenu.add(autoZoomPopup);
 
+        autoLoad = TMSPreferences.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);
@@ -164,6 +228,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
 
         tileOptionMenu.add(new JMenuItem(new AbstractAction(
                 tr("Show Tile Info")) {
+            @Override
             public void actionPerformed(ActionEvent ae) {
                 out("info tile: " + clickedTile);
                 if (clickedTile != null) {
@@ -186,6 +251,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
 
         tileOptionMenu.add(new JMenuItem(new AbstractAction(
                 tr("Load All Tiles")) {
+            @Override
             public void actionPerformed(ActionEvent ae) {
                 loadAllTiles(true);
                 redraw();
@@ -195,6 +261,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         // increase and decrease commands
         tileOptionMenu.add(new JMenuItem(
                 new AbstractAction(tr("Increase zoom")) {
+                    @Override
                     public void actionPerformed(ActionEvent ae) {
                         increaseZoomLevel();
                         redraw();
@@ -203,6 +270,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
 
         tileOptionMenu.add(new JMenuItem(
                 new AbstractAction(tr("Decrease zoom")) {
+                    @Override
                     public void actionPerformed(ActionEvent ae) {
                         decreaseZoomLevel();
                         redraw();
@@ -213,6 +281,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
 
         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");
@@ -229,6 +298,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
 
         tileOptionMenu.add(new JMenuItem(
                 new AbstractAction(tr("Flush Tile Cache")) {
+                    @Override
                     public void actionPerformed(ActionEvent ae) {
                         System.out.print("flushing all tiles...");
                         clearTileCache();
@@ -238,26 +308,54 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         // 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)
-                            return;
-                        clickedTile = getTileForPixelpos(e.getX(), e.getY());
-                        tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
+                        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);
                     }
@@ -266,6 +364,10 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         });
     }
 
+    public static boolean isUrlWithPatterns(String url) {
+        return url != null && url.contains("{") && url.contains("}");
+    }
+
     void zoomChanged()
     {
         if (debug)
@@ -273,23 +375,20 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         needRedraw = true;
         jobDispatcher.cancelOutstandingJobs();
         tileRequestsOutstanding.clear();
-        SlippyMapPreferences.setLastZoom(currentZoomLevel);
     }
 
     int getMaxZoomLvl()
     {
-        int ret = SlippyMapPreferences.getMaxZoomLvl();
-        if (tileSource.getMaxZoom() < ret)
-            ret = tileSource.getMaxZoom();
-        return ret;
+        if (info.getMaxZoom() != 0) {
+            return TMSPreferences.checkMaxZoomLvl(info.getMaxZoom(), tileSource);
+        } else {
+            return TMSPreferences.getMaxZoomLvl(tileSource);
+        }
     }
 
     int getMinZoomLvl()
     {
-        int ret = SlippyMapPreferences.getMinZoomLvl();
-        if (tileSource.getMinZoom() > ret)
-            ret = tileSource.getMinZoom();
-        return ret;
+        return TMSPreferences.getMinZoomLvl(tileSource);
     }
 
     /**
@@ -400,8 +499,8 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
 
     void loadAllTiles(boolean force) {
         MapView mv = Main.map.mapView;
-        LatLon topLeft = mv.getLatLon(0, 0);
-        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
 
         TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
 
@@ -419,6 +518,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
      * 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;
@@ -594,10 +694,9 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
                         img_x_offset, img_y_offset,
                         img_x_end, img_y_end,
                         this);
-        float fadeBackground = SlippyMapPreferences.getFadeBackground();
-        if (fadeBackground != 0f) {
+        if (ImageryPreferences.PROP_FADE_AMOUNT.get() != 0) {
             // dimm by painting opaque rect...
-            g.setColor(new Color(1f, 1f, 1f, fadeBackground));
+            g.setColor(ImageryPreferences.getFadeColorWithAlpha());
             g.fillRect(target.x, target.y,
                        target.width, target.height);
         }
@@ -645,7 +744,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         Point p = pixelPos(t);
         int texty = p.y + 2 + fontHeight;
 
-        if (SlippyMapPreferences.getDrawDebug()) {
+        if (TMSPreferences.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)) {
@@ -670,7 +769,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
 
         int xCursor = -1;
         int yCursor = -1;
-        if (SlippyMapPreferences.getDrawDebug()) {
+        if (TMSPreferences.PROP_DRAW_DEBUG.get()) {
             if (yCursor < t.getYtile()) {
                 if (t.getYtile() % 32 == 31) {
                     g.fillRect(0, p.y - 1, mv.getWidth(), 3);
@@ -683,34 +782,46 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
             // column. Only draw them for the top tile in
             // the column.
             if (xCursor < t.getXtile()) {
-                if (SlippyMapPreferences.getDrawDebug()) {
-                    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());
-                    }
+                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();
             }
         }
     }
 
-    public Point pixelPos(LatLon ll) {
-        return Main.map.mapView.getPoint(ll);
+    private Point pixelPos(LatLon ll) {
+        return Main.map.mapView.getPoint(Main.proj.latlon2eastNorth(ll).add(getDx(), getDy()));
     }
-    public Point pixelPos(Tile t) {
+    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 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);
@@ -781,8 +892,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         void loadAllTiles(boolean force)
         {
             List<Tile> tiles = this.allTiles(true);
-            boolean autoload = SlippyMapPreferences.getAutoloadTiles();
-            if (!autoload && !force)
+            if (!autoLoad && !force)
                return;
             int nr_queued = 0;
             for (Tile t : tiles) {
@@ -800,24 +910,25 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
     {
         if (az_disable)
             return false;
-        return autoZoomPopup.isSelected();
+        return autoZoom;
     }
     /**
      */
     @Override
     public void paint(Graphics2D g, MapView mv, Bounds bounds) {
         //long start = System.currentTimeMillis();
-        LatLon topLeft = mv.getLatLon(0, 0);
-        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
         Graphics2D oldg = g;
 
-        if (botRight.lon() == 0.0 || botRight.lat() == 0) {
+        if (botRight.east() == 0.0 || botRight.north() == 0) {
             Main.debug("still initializing??");
             // probably still initializing
             return;
         }
-        if (lastTopLeft != null && lastBotRight != null && topLeft.equalsEpsilon(lastTopLeft)
-                && botRight.equalsEpsilon(lastBotRight) && bufferImage != null
+
+        if (lastTopLeft != null && lastBotRight != null && topLeft.equals(lastTopLeft)
+                && botRight.equals(lastBotRight) && bufferImage != null
                 && mv.getWidth() == bufferImage.getWidth(null) && mv.getHeight() == bufferImage.getHeight(null)
                 && !needRedraw) {
 
@@ -880,7 +991,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         for (int zoomOffset : otherZooms) {
             if (!autoZoomEnabled())
                 break;
-            if (!SlippyMapPreferences.getAutoloadTiles())
+            if (!autoLoad)
                 break;
             int newzoom = currentZoomLevel + zoomOffset;
             if (missedTiles.size() <= 0)
@@ -907,6 +1018,53 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         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,
+                    getShiftedLatLon(topLeft), getShiftedLatLon(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);
+        }
+
         oldg.drawImage(bufferImage, 0, 0, null);
 
         if (autoZoomEnabled() && lastImageScale != null) {
@@ -946,8 +1104,8 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
             out("getTileForPixelpos("+px+", "+py+")");
         MapView mv = Main.map.mapView;
         Point clicked = new Point(px, py);
-        LatLon topLeft = mv.getLatLon(0, 0);
-        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+        EastNorth topLeft = mv.getEastNorth(0, 0);
+        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
         int z = currentZoomLevel;
         TileSet ts = new TileSet(topLeft, botRight, z);
 
@@ -973,16 +1131,6 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
     }
 
     @Override
-    public Icon getIcon() {
-        return ImageProvider.get("slippymap");
-    }
-
-    @Override
-    public Object getInfoComponent() {
-        return null;
-    }
-
-    @Override
     public Action[] getMenuEntries() {
         return new Action[] {
                 LayerListDialog.getInstance().createShowHideLayerAction(),
@@ -1000,15 +1148,6 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
     }
 
     @Override
-    public boolean isMergable(Layer other) {
-        return false;
-    }
-
-    @Override
-    public void mergeFrom(Layer from) {
-    }
-
-    @Override
     public void visitBoundingBox(BoundingXYVisitor v) {
     }
 
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSPreferences.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSPreferences.java
new file mode 100644
index 0000000..b2d62a3
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSPreferences.java
@@ -0,0 +1,82 @@
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+
+/**
+ * Preferences for Slippy Map Tiles
+ *
+ * @author Hakan Tandogan <hakan at gurkensalat.com>
+ * @author LuVar <lubomir.varga at freemap.sk>
+ * @author Upliner <upliner at gmail.com>
+ *
+ */
+public class TMSPreferences
+{
+    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);
+
+    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);
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSTileSource.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSTileSource.java
new file mode 100644
index 0000000..fcd2878
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSTileSource.java
@@ -0,0 +1,22 @@
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+import org.openstreetmap.gui.jmapviewer.OsmTileSource;
+
+public class TMSTileSource extends OsmTileSource.AbstractOsmTileSource {
+    private int maxZoom;
+
+    public TMSTileSource(String name, String url, int maxZoom) {
+        super(name, url);
+        this.maxZoom = maxZoom;
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return (maxZoom == 0) ? super.getMaxZoom() : maxZoom;
+    }
+
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+}
\ No newline at end of file
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TemplatedTMSTileSource.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TemplatedTMSTileSource.java
new file mode 100644
index 0000000..5fc00b5
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TemplatedTMSTileSource.java
@@ -0,0 +1,30 @@
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+import org.openstreetmap.gui.jmapviewer.OsmTileSource;
+
+public class TemplatedTMSTileSource extends OsmTileSource.AbstractOsmTileSource {
+    private int maxZoom;
+    
+    public TemplatedTMSTileSource(String name, String url, int maxZoom) {
+        super(name, url);
+        this.maxZoom = maxZoom;
+    }
+
+    public String getTileUrl(int zoom, int tilex, int tiley) {
+        return this.BASE_URL
+        .replaceAll("\\{zoom\\}", Integer.toString(zoom))
+        .replaceAll("\\{x\\}", Integer.toString(tilex))
+        .replaceAll("\\{y\\}", Integer.toString(tiley));
+        
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return (maxZoom == 0) ? super.getMaxZoom() : maxZoom;
+    }
+
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+}
\ No newline at end of file
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/AddWMSLayerPanel.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/AddWMSLayerPanel.java
new file mode 100644
index 0000000..a66fb68
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/AddWMSLayerPanel.java
@@ -0,0 +1,530 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+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;
+        }
+    }
+
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/GeorefImage.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/GeorefImage.java
new file mode 100644
index 0000000..a18639b
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/GeorefImage.java
@@ -0,0 +1,248 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+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.plugins.imagery.ImageryPreferences;
+
+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();
+            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("Exception occurred"), 10, img.getHeight()/2);
+            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:
+            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 (ImageryPreferences.PROP_FADE_AMOUNT.get() == 0)
+            newFadeColor = transparentColor;
+        else
+            newFadeColor = ImageryPreferences.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;
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Grabber.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Grabber.java
new file mode 100644
index 0000000..346f26f
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Grabber.java
@@ -0,0 +1,115 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.plugins.imagery.ImageryPlugin;
+import org.openstreetmap.josm.plugins.imagery.wms.GeorefImage.State;
+
+abstract public class Grabber implements Runnable {
+    protected final MapView mv;
+    protected final WMSLayer layer;
+    protected final CacheFiles cache;
+
+    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;
+        this.cache = cache;
+    }
+
+    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 && ImageryPlugin.wmsAdapter.PROP_OVERLAP.get()) {
+            double eastSize =  b.max.east() - b.min.east();
+            double northSize =  b.max.north() - b.min.north();
+
+            double eastCoef = ImageryPlugin.wmsAdapter.PROP_OVERLAP_EAST.get() / 100.0;
+            double northCoef = ImageryPlugin.wmsAdapter.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;
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/HTMLGrabber.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/HTMLGrabber.java
new file mode 100644
index 0000000..16c9bcb
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/HTMLGrabber.java
@@ -0,0 +1,46 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+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.Main;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.io.CacheFiles;
+
+public class HTMLGrabber extends WMSGrabber {
+    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(
+                Main.pref.get("wmsplugin.browser", "webkit-image {0}"), 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;
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Map_Rectifier_WMSmenuAction.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Map_Rectifier_WMSmenuAction.java
new file mode 100644
index 0000000..b4f8a26
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Map_Rectifier_WMSmenuAction.java
@@ -0,0 +1,238 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+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.actions.JosmAction;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo;
+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();
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSAdapter.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSAdapter.java
new file mode 100644
index 0000000..cebf2ef
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSAdapter.java
@@ -0,0 +1,40 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.plugins.imagery.wms.io.WMSLayerExporter;
+import org.openstreetmap.josm.plugins.imagery.wms.io.WMSLayerImporter;
+
+// WMSPlugin-specific functions
+public class WMSAdapter {
+    CacheFiles cache = new CacheFiles("wmsplugin");
+
+    public final IntegerProperty PROP_SIMULTANEOUS_CONNECTIONS = new IntegerProperty("wmsplugin.simultaneousConnections", 3);
+    public final BooleanProperty PROP_OVERLAP = new BooleanProperty("wmsplugin.url.overlap", false);
+    public final IntegerProperty PROP_OVERLAP_EAST = new IntegerProperty("wmsplugin.url.overlapEast", 14);
+    public final IntegerProperty PROP_OVERLAP_NORTH = new IntegerProperty("wmsplugin.url.overlapNorth", 4);
+
+    protected void initExporterAndImporter() {
+        ExtensionFileFilter.exporters.add(new WMSLayerExporter());
+        ExtensionFileFilter.importers.add(new WMSLayerImporter());
+    }
+
+    public WMSAdapter() {
+        cache.setExpire(CacheFiles.EXPIRE_MONTHLY, false);
+        cache.setMaxSize(70, false);
+        initExporterAndImporter();
+    }
+
+    public Grabber getGrabber(MapView mv, WMSLayer layer){
+        if(layer.getInfo().getImageryType() == ImageryType.HTML)
+            return new HTMLGrabber(mv, layer, cache);
+        else if(layer.getInfo().getImageryType() == ImageryType.WMS)
+            return new WMSGrabber(mv, layer, cache);
+        else throw new IllegalStateException("WMSAdapter.getGrabber() called for non-WMS layer type");
+    }
+
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSGrabber.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSGrabber.java
new file mode 100644
index 0000000..d0d199c
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSGrabber.java
@@ -0,0 +1,203 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+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.projection.Mercator;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.openstreetmap.josm.io.ProgressInputStream;
+import org.openstreetmap.josm.plugins.imagery.wms.GeorefImage.State;
+
+
+public class WMSGrabber extends Grabber {
+    public static boolean isUrlWithPatterns(String url) {
+        return url != null && url.contains("{") && url.contains("}");
+    }
+
+    protected String baseURL;
+    private final boolean urlWithPatterns;
+
+    WMSGrabber(MapView mv, WMSLayer layer, CacheFiles cache) {
+        super(mv, layer, cache);
+        this.baseURL = layer.getInfo().getURL();
+        /* URL containing placeholders? */
+        urlWithPatterns = 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) // don't use mercator code
+            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("wmsplugin.user_agent", Version.getInstance().getAgentString()));
+        conn.setConnectTimeout(Main.pref.getInteger("wmsplugin.timeout.connect", 30) * 1000);
+        conn.setReadTimeout(Main.pref.getInteger("wmsplugin.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();
+    }
+}
diff --git a/wmsplugin/src/wmsplugin/WMSLayer.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSLayer.java
similarity index 90%
copy from wmsplugin/src/wmsplugin/WMSLayer.java
copy to imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSLayer.java
index 716c2f2..de20cbc 100644
--- a/wmsplugin/src/wmsplugin/WMSLayer.java
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSLayer.java
@@ -1,11 +1,10 @@
-package wmsplugin;
+package org.openstreetmap.josm.plugins.imagery.wms;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.Component;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
-import java.awt.Toolkit;
 import java.awt.event.ActionEvent;
 import java.io.File;
 import java.io.FileInputStream;
@@ -22,8 +21,6 @@ import java.util.concurrent.locks.ReentrantLock;
 
 import javax.swing.AbstractAction;
 import javax.swing.Action;
-import javax.swing.Icon;
-import javax.swing.ImageIcon;
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JFileChooser;
 import javax.swing.JOptionPane;
@@ -43,24 +40,23 @@ import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.plugins.imagery.ImageryLayer;
+import org.openstreetmap.josm.plugins.imagery.ImageryPlugin;
+import org.openstreetmap.josm.plugins.imagery.wms.GeorefImage.State;
 import org.openstreetmap.josm.tools.ImageProvider;
 
-import wmsplugin.GeorefImage.State;
-
 /**
  * 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 Layer implements PreferenceChangedListener {
-
-    protected static final Icon icon =
-        new ImageIcon(Toolkit.getDefaultToolkit().createImage(WMSPlugin.class.getResource("/images/wms_small.png")));
+public class WMSLayer extends ImageryLayer implements PreferenceChangedListener {
 
     public static final BooleanProperty PROP_ALPHA_CHANNEL = new BooleanProperty("wmsplugin.alpha_channel", true);
-    WMSPlugin plugin = WMSPlugin.instance;
+    WMSAdapter plugin = ImageryPlugin.wmsAdapter;
 
     public int messageNum = 5; //limit for messages per layer
-    protected MapView mv;
     protected String resolution;
     protected int imageSize = 500;
     protected int dax = 10;
@@ -68,14 +64,10 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
     protected int daStep = 5;
     protected int minZoom = 3;
 
-    protected double dx = 0.0;
-    protected double dy = 0.0;
-
     protected GeorefImage[][] images;
     protected final int serializeFormatVersion = 5;
     protected boolean autoDownloadEnabled = true;
     protected boolean settingsChanged;
-    protected WMSInfo info;
 
     // Image index boundary for current view
     private volatile int bminx;
@@ -103,26 +95,25 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
     private boolean isInvalidUrlConfirmed = false;
 
     public WMSLayer() {
-        this(new WMSInfo(tr("Blank Layer")));
+        this(new ImageryInfo(tr("Blank Layer")));
     }
 
-    public WMSLayer(WMSInfo info) {
-        super(info.name);
+    public WMSLayer(ImageryInfo info) {
+        super(info);
         setBackgroundLayer(true); /* set global background variable */
         initializeImages();
-        this.info = new WMSInfo(info);
-        mv = Main.map.mapView;
-        if(this.info.pixelPerDegree == 0.0)
+        this.info = new ImageryInfo(info);
+        if(this.info.getPixelPerDegree() == 0.0)
             this.info.setPixelPerDegree(getPPD());
         resolution = mv.getDist100PixelText();
 
-        if(info.url != null) {
-            WMSGrabber.getProjection(info.url, true);
+        if(info.getURL() != null) {
+            WMSGrabber.getProjection(info.getURL(), true);
             startGrabberThreads();
-            if(!info.url.startsWith("html:") && !WMSGrabber.isUrlWithPatterns(info.url)) {
-                if (!(info.url.endsWith("&") || info.url.endsWith("?"))) {
-                    if (!confirmMalformedUrl(info.url)) {
-                        System.out.println(tr("Warning: WMS layer deactivated because of malformed base url ''{0}''", info.url));
+            if(info.getImageryType() == ImageryType.WMS && !WMSGrabber.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;
@@ -138,7 +129,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
 
     public void doSetName(String name) {
         setName(name);
-        info.name = name;
+        info.setName(name);
     }
 
     public boolean hasAutoDownload(){
@@ -172,10 +163,6 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         }
     }
 
-    @Override public Icon getIcon() {
-        return icon;
-    }
-
     @Override public String getToolTipText() {
         if(autoDownloadEnabled)
             return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolution);
@@ -196,11 +183,11 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
 
     private boolean zoomIsTooBig() {
         //don't download when it's too outzoomed
-        return info.pixelPerDegree / getPPD() > minZoom;
+        return info.getPixelPerDegree() / getPPD() > minZoom;
     }
 
     @Override public void paint(Graphics2D g, final MapView mv, Bounds b) {
-        if(info.url == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
+        if(info.getURL() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
 
         settingsChanged = false;
 
@@ -253,40 +240,35 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         }
     }
 
-    public double getPPD(){
-        ProjectionBounds bounds = mv.getProjectionBounds();
-        return mv.getWidth() / (bounds.max.east() - bounds.min.east());
-    }
-
+    @Override
     public void displace(double dx, double dy) {
+        super.displace(dx, dy);
         settingsChanged = true;
-        this.dx += dx;
-        this.dy += dy;
     }
 
     public int getImageXIndex(double coord) {
-        return (int)Math.floor( ((coord - dx) * info.pixelPerDegree) / imageSize);
+        return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize);
     }
 
     public int getImageYIndex(double coord) {
-        return (int)Math.floor( ((coord - dy) * info.pixelPerDegree) / imageSize);
+        return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize);
     }
 
     public int getImageX(int imageIndex) {
-        return (int)(imageIndex * imageSize * (getPPD() / info.pixelPerDegree) + dx * getPPD());
+        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD());
     }
 
     public int getImageY(int imageIndex) {
-        return (int)(imageIndex * imageSize * (getPPD() / info.pixelPerDegree) + dy * getPPD());
+        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD());
     }
 
     public int getImageWidth(int xIndex) {
-        int overlap = (int)(plugin.PROP_OVERLAP.get()?plugin.PROP_OVERLAP_EAST.get() * imageSize * getPPD() / info.pixelPerDegree / 100:0);
+        int overlap = (int)(plugin.PROP_OVERLAP.get()?plugin.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)(plugin.PROP_OVERLAP.get()?plugin.PROP_OVERLAP_NORTH.get() * imageSize * getPPD() / info.pixelPerDegree / 100:0);
+        int overlap = (int)(plugin.PROP_OVERLAP.get()?plugin.PROP_OVERLAP_NORTH.get() * imageSize * getPPD() / info.getPixelPerDegree() / 100:0);
         return getImageY(yIndex + 1) - getImageY(yIndex) + overlap;
     }
 
@@ -316,7 +298,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
      * @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.pixelPerDegree, (yIndex * imageSize) / info.pixelPerDegree);
+        return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree());
     }
 
 
@@ -351,7 +333,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
             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.pixelPerDegree, real);
+                    WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real);
                     addRequest(request);
                 }
             }
@@ -368,10 +350,6 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         }
     }
 
-    @Override public Object getInfoComponent() {
-        return getToolTipText();
-    }
-
     @Override public Action[] getMenuEntries() {
         return new Action[]{
                 LayerListDialog.getInstance().createActivateLayerAction(this),
@@ -409,7 +387,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
      * @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.pixelPerDegree) {
+        if (request.getPixelPerDegree() != info.getPixelPerDegree()) {
             return -1;
         }
         if (bminx > request.getXIndex()
@@ -519,6 +497,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         public DownloadAction() {
             super(tr("Download visible tiles"));
         }
+        @Override
         public void actionPerformed(ActionEvent ev) {
             if (zoomIsTooBig()) {
                 JOptionPane.showMessageDialog(
@@ -537,6 +516,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         public ChangeResolutionAction() {
             super(tr("Change resolution"));
         }
+        @Override
         public void actionPerformed(ActionEvent ev) {
             initializeImages();
             resolution = mv.getDist100PixelText();
@@ -550,6 +530,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         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
@@ -559,7 +540,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
                 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.pixelPerDegree, true));
+                        addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true));
                         mv.repaint();
                     }
                 }
@@ -571,6 +552,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         public ToggleAlphaAction() {
             super(tr("Alpha channel"));
         }
+        @Override
         public void actionPerformed(ActionEvent ev) {
             JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
             boolean alphaChannel = checkbox.isSelected();
@@ -585,11 +567,13 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
             }
             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;
         }
@@ -599,6 +583,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         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");
@@ -611,8 +596,8 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
                     oos.writeInt(dax);
                     oos.writeInt(day);
                     oos.writeInt(imageSize);
-                    oos.writeDouble(info.pixelPerDegree);
-                    oos.writeObject(info.name);
+                    oos.writeDouble(info.getPixelPerDegree());
+                    oos.writeObject(info.getName());
                     oos.writeObject(info.getFullURL());
                     oos.writeObject(images);
                     oos.close();
@@ -627,6 +612,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         public LoadWmsAction() {
             super(tr("Load WMS layer from file"), ImageProvider.get("load"));
         }
+        @Override
         public void actionPerformed(ActionEvent ev) {
             JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true,
                     false, tr("Load WMS layer"), "wms");
@@ -664,7 +650,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
                 }
                 settingsChanged = true;
                 mv.repaint();
-                if(info.url != null)
+                if(info.getURL() != null)
                 {
                     startGrabberThreads();
                 }
@@ -689,8 +675,9 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         public BookmarkWmsAction() {
             super(tr("Set WMS Bookmark"));
         }
+        @Override
         public void actionPerformed(ActionEvent ev) {
-            plugin.addLayer(new WMSInfo(info));
+            ImageryPlugin.instance.addLayer(new ImageryInfo(info));
         }
     }
 
@@ -700,16 +687,19 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
             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) {
@@ -773,6 +763,7 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         }
     }
 
+    @Override
     public void preferenceChanged(PreferenceChangeEvent event) {
         if (event.getKey().equals(plugin.PROP_SIMULTANEOUS_CONNECTIONS.getKey())) {
             cancelGrabberThreads(true);
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRequest.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRequest.java
new file mode 100644
index 0000000..8024fb2
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRequest.java
@@ -0,0 +1,102 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import java.awt.image.BufferedImage;
+
+import org.openstreetmap.josm.plugins.imagery.wms.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;
+    }
+}
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerExporter.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerExporter.java
new file mode 100644
index 0000000..53d9dbd
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerExporter.java
@@ -0,0 +1,13 @@
+package org.openstreetmap.josm.plugins.imagery.wms.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.io.FileExporter;
+
+public class WMSLayerExporter extends FileExporter{
+
+    public WMSLayerExporter() {
+        super(new ExtensionFileFilter("wms", "wms", tr("WMS Files (*.wms)")));
+    }
+}
\ No newline at end of file
diff --git a/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerImporter.java b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerImporter.java
new file mode 100644
index 0000000..4e497f6
--- /dev/null
+++ b/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerImporter.java
@@ -0,0 +1,14 @@
+package org.openstreetmap.josm.plugins.imagery.wms.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.io.FileImporter;
+
+public class WMSLayerImporter extends FileImporter{
+
+    public WMSLayerImporter() {
+        super(new ExtensionFileFilter("wms", "wms", tr("WMS Files (*.wms)")));
+    }
+
+}
diff --git a/routing/build.xml b/routing/build.xml
index ecc98b4..72ae19d 100644
--- a/routing/build.xml
+++ b/routing/build.xml
@@ -27,7 +27,7 @@
 <project name="routing" default="dist" basedir=".">
 
 	<property name="commit.message" value="Changed the constructor signature of the plugin main class" />
-	<property name="plugin.main.version" value="3408" />
+	<property name="plugin.main.version" value="3600" />
 
 	<!-- Define some properties -->
 	<property name="josm"                   location="../../core/dist/josm-custom.jar"/>
diff --git a/routing/src/com/innovant/josm/plugin/routing/RoutingLayer.java b/routing/src/com/innovant/josm/plugin/routing/RoutingLayer.java
index 6cd0eab..cc879bc 100644
--- a/routing/src/com/innovant/josm/plugin/routing/RoutingLayer.java
+++ b/routing/src/com/innovant/josm/plugin/routing/RoutingLayer.java
@@ -130,6 +130,7 @@ public class RoutingLayer extends Layer {
      */
     public final Node getNearestHighwayNode(Point p) {
         Node nearest = null;
+        int snapDistance = NavigatableComponent.PROP_SNAP_DISTANCE.get();
         double minDist = 0;
         for (Way w : dataLayer.data.getWays()) {
             if (w.isDeleted() || w.isIncomplete() || w.get("highway")==null) continue;
@@ -138,7 +139,7 @@ public class RoutingLayer extends Layer {
 
                 Point P = Main.map.mapView.getPoint(n);
                 double dist = p.distanceSq(P);
-                if (dist < NavigatableComponent.snapDistance) {
+                if (dist < snapDistance) {
                     if ((nearest == null) || (dist < minDist)) {
                         nearest = n;
                         minDist = dist;
diff --git a/routing/src/com/innovant/josm/plugin/routing/gui/RoutingPreferenceDialog.java b/routing/src/com/innovant/josm/plugin/routing/gui/RoutingPreferenceDialog.java
index 61b7f94..0bd178f 100644
--- a/routing/src/com/innovant/josm/plugin/routing/gui/RoutingPreferenceDialog.java
+++ b/routing/src/com/innovant/josm/plugin/routing/gui/RoutingPreferenceDialog.java
@@ -28,6 +28,8 @@
 
 package com.innovant.josm.plugin.routing.gui;
 
+import static org.openstreetmap.josm.tools.I18n.tr;
+
 import java.awt.ComponentOrientation;
 import java.awt.Dimension;
 import java.awt.GridBagLayout;
@@ -50,17 +52,13 @@ import javax.swing.JTable;
 import javax.swing.JTextField;
 import javax.swing.table.DefaultTableModel;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
-
 import org.apache.log4j.Logger;
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.gui.preferences.PreferenceDialog;
 import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
 import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
 import org.openstreetmap.josm.tools.GBC;
 
 import com.innovant.josm.jrt.osm.OsmWayTypes;
-import com.innovant.josm.plugin.routing.RoutingPlugin;
 
 public class RoutingPreferenceDialog implements PreferenceSetting {
 
diff --git a/slippymap/.classpath b/slippymap/.classpath
index 17b8e6a..663bc1b 100644
--- a/slippymap/.classpath
+++ b/slippymap/.classpath
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
 	<classpathentry kind="src" path="src"/>
-	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JDK 5"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/JOSM"/>
 	<classpathentry kind="output" path="build"/>
 </classpath>
diff --git a/slippymap/build.xml b/slippymap/build.xml
index 3151356..ebcfec7 100644
--- a/slippymap/build.xml
+++ b/slippymap/build.xml
@@ -26,8 +26,8 @@
 -->
 <project name="slippymap" default="dist" basedir=".">
 
-	<property name="commit.message" value="Added haiti imagery tile source" />
-	<property name="plugin.main.version" value="3408" />
+	<property name="commit.message" value="change ToU to the osm specific one (like it is done in Potlatch 2)" />
+	<property name="plugin.main.version" value="3687" />
 
 	<property name="josm"                   location="../../core/dist/josm-custom.jar"/>
 	<property name="plugin.dist.dir"        value="../../dist"/>
diff --git a/slippymap/images/bing_maps.png b/slippymap/images/bing_maps.png
new file mode 100644
index 0000000..ae4367e
Binary files /dev/null and b/slippymap/images/bing_maps.png differ
diff --git a/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapLayer.java b/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapLayer.java
index 1a1fb83..daae3ef 100644
--- a/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapLayer.java
+++ b/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapLayer.java
@@ -3,6 +3,7 @@ package org.openstreetmap.josm.plugins.slippymap;
 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;
@@ -12,8 +13,14 @@ 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.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;
@@ -103,6 +110,16 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
     private JPopupMenu tileOptionMenu;
     JCheckBoxMenuItem autoZoomPopup;
     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);
+    }
 
     void redraw()
     {
@@ -114,6 +131,18 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
     {
         int origZoom = currentZoomLevel;
         tileSource = SlippyMapPreferences.getMapSource();
+        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();
+        }
+        
         // The minimum should also take care of integer parsing
         // errors which would leave us with a zoom of -1 otherwise
         if (tileSource.getMaxZoom() < currentZoomLevel)
@@ -242,10 +271,34 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
                 Main.map.mapView.addMouseListener(new MouseAdapter() {
                     @Override
                     public void mouseClicked(MouseEvent e) {
-                        if (e.getButton() != MouseEvent.BUTTON3)
-                            return;
-                        clickedTile = getTileForPixelpos(e.getX(), e.getY());
-                        tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
+                        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();
+                                }
+                            }
+                        }
                     }
                 });
 
@@ -816,6 +869,7 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
             // probably still initializing
             return;
         }
+
         if (lastTopLeft != null && lastBotRight != null && topLeft.equalsEpsilon(lastTopLeft)
                 && botRight.equalsEpsilon(lastBotRight) && bufferImage != null
                 && mv.getWidth() == bufferImage.getWidth(null) && mv.getHeight() == bufferImage.getHeight(null)
@@ -907,6 +961,43 @@ public class SlippyMapLayer extends Layer implements ImageObserver,
         for (Tile t : ts.allTiles()) {
             this.paintTileText(ts, t, g, mv, currentZoomLevel, t);
         }
+
+        if (tileSource.requiresAttribution()) {
+            // Draw attribution
+            g.setColor(Color.white);
+            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.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, topLeft, botRight);
+            Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
+            g.drawString(attributionText, mv.getWidth() - (int) stringBounds.getWidth(), mv.getHeight() - textHeight);
+            
+            g.setFont(font);
+        }
+
         oldg.drawImage(bufferImage, 0, 0, null);
 
         if (autoZoomEnabled() && lastImageScale != null) {
diff --git a/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapPlugin.java b/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapPlugin.java
index 008210a..018e184 100644
--- a/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapPlugin.java
+++ b/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapPlugin.java
@@ -1,11 +1,18 @@
 package org.openstreetmap.josm.plugins.slippymap;
 
+import java.util.ArrayList;
 import java.util.List;
 
+import org.openstreetmap.gui.jmapviewer.OsmTileSource.CycleMap;
+import org.openstreetmap.gui.jmapviewer.OsmTileSource.Mapnik;
+import org.openstreetmap.gui.jmapviewer.OsmTileSource.TilesAtHome;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
 import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
 import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
+import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser.TileSourceProvider;
 import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
 import org.openstreetmap.josm.plugins.Plugin;
 import org.openstreetmap.josm.plugins.PluginInformation;
@@ -18,67 +25,79 @@ import org.openstreetmap.josm.plugins.PluginInformation;
  */
 public class SlippyMapPlugin extends Plugin implements PreferenceChangedListener
 {
-    public SlippyMapPlugin(PluginInformation info)
-    {
-        super(info);
-        Main.pref.addPreferenceChangeListener(this);
-    }
+	public SlippyMapPlugin(PluginInformation info)
+	{
+		super(info);
+		Main.pref.addPreferenceChangeListener(this);
+		SlippyMapBBoxChooser.addTileSourceProvider(new TileSourceProvider() {
+			public List<TileSource> getTileSources() {
+				List<TileSource> result = new ArrayList<TileSource>();
+				for (TileSource ts: SlippyMapPreferences.getAllMapSources()) {
+					if (ts instanceof Mapnik || ts instanceof CycleMap || ts instanceof TilesAtHome) {
+						continue; // Already included in default list
+					}
+					result.add(ts);
+				}
+				return result;
+			}
+		});
+	}
 
-    @Override
-    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame)
-    {
-        if (newFrame != null && SlippyMapPreferences.getMapSource() != SlippyMapPreferences.NO_DEFAULT_TILE_SOURCE) {
-            SlippyMapLayer smlayer;
-            smlayer = new SlippyMapLayer();
-            Main.main.addLayer(smlayer);
-        }
-    }
+	@Override
+	public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame)
+	{
+		if (newFrame != null && SlippyMapPreferences.getMapSource() != SlippyMapPreferences.NO_DEFAULT_TILE_SOURCE) {
+			SlippyMapLayer smlayer;
+			smlayer = new SlippyMapLayer();
+			Main.main.addLayer(smlayer);
+		}
+	}
 
-    /*
-     * (non-Javadoc)
-     *
-     * @see org.openstreetmap.josm.plugins.Plugin#getPreferenceSetting()
-     */
-    @Override
-    public PreferenceSetting getPreferenceSetting()
-    {
-        return new SlippyMapPreferenceSetting();
-    }
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @see org.openstreetmap.josm.plugins.Plugin#getPreferenceSetting()
+	 */
+	@Override
+	public PreferenceSetting getPreferenceSetting()
+	{
+		return new SlippyMapPreferenceSetting();
+	}
 
-    /*
-     * (non-Javadoc)
-     *
-     * @seeorg.openstreetmap.josm.data.Preferences.PreferenceChangedListener#
-     * preferenceChanged(java.lang.String, java.lang.String)
-     */
-    public void preferenceChanged(PreferenceChangeEvent event) {
-        if (!Main.isDisplayingMapView()) {
-            return;
-        }
-        List<SlippyMapLayer> layes = Main.map.mapView.getLayersOfType(SlippyMapLayer.class);
-        assert layes.size() <= 1;
-        SlippyMapLayer layer = layes.isEmpty()?null:layes.get(0);
+	/*
+	 * (non-Javadoc)
+	 *
+	 * @seeorg.openstreetmap.josm.data.Preferences.PreferenceChangedListener#
+	 * preferenceChanged(java.lang.String, java.lang.String)
+	 */
+	public void preferenceChanged(PreferenceChangeEvent event) {
+		if (!Main.isDisplayingMapView()) {
+			return;
+		}
+		List<SlippyMapLayer> layes = Main.map.mapView.getLayersOfType(SlippyMapLayer.class);
+		assert layes.size() <= 1;
+		SlippyMapLayer layer = layes.isEmpty()?null:layes.get(0);
 
-        if (event.getKey().equals(SlippyMapPreferences.PREFERENCE_TILE_SOURCE)) {
-            if (layer == null && SlippyMapPreferences.getMapSource() != SlippyMapPreferences.NO_DEFAULT_TILE_SOURCE) {
-                Main.map.mapView.addLayer(new SlippyMapLayer());
-            } else if (layer != null && SlippyMapPreferences.getMapSource() == SlippyMapPreferences.NO_DEFAULT_TILE_SOURCE) {
-                Main.map.mapView.removeLayer(layer);
-            } else if (layer == null && SlippyMapPreferences.getMapSource() == SlippyMapPreferences.NO_DEFAULT_TILE_SOURCE) {
-                // Do nothing
-            } else {
-                layer.newTileStorage();
-            }
-        } else  if (event.getKey().startsWith(SlippyMapPreferences.PREFERENCE_PREFIX) && layer != null) {
-            // System.err.println(this + ".preferenceChanged('" + key + "', '"
-            // + newValue + "') called");
-            // when fade background changed, no need to clear tile storage
-            // TODO move this code to SlippyMapPreferences class.
-            if (!event.getKey().equals(SlippyMapPreferences.PREFERENCE_FADE_BACKGROUND)) {
-                layer.autoZoomPopup.setSelected(SlippyMapPreferences.getAutozoom());
-            }
-            layer.redraw();
-        }
-    }
+		if (event.getKey().equals(SlippyMapPreferences.PREFERENCE_TILE_SOURCE)) {
+			if (layer == null && SlippyMapPreferences.getMapSource() != SlippyMapPreferences.NO_DEFAULT_TILE_SOURCE) {
+				Main.map.mapView.addLayer(new SlippyMapLayer());
+			} else if (layer != null && SlippyMapPreferences.getMapSource() == SlippyMapPreferences.NO_DEFAULT_TILE_SOURCE) {
+				Main.map.mapView.removeLayer(layer);
+			} else if (layer == null && SlippyMapPreferences.getMapSource() == SlippyMapPreferences.NO_DEFAULT_TILE_SOURCE) {
+				// Do nothing
+			} else {
+				layer.newTileStorage();
+			}
+		} else  if (event.getKey().startsWith(SlippyMapPreferences.PREFERENCE_PREFIX) && layer != null) {
+			// System.err.println(this + ".preferenceChanged('" + key + "', '"
+			// + newValue + "') called");
+			// when fade background changed, no need to clear tile storage
+			// TODO move this code to SlippyMapPreferences class.
+			if (!event.getKey().equals(SlippyMapPreferences.PREFERENCE_FADE_BACKGROUND)) {
+				layer.autoZoomPopup.setSelected(SlippyMapPreferences.getAutozoom());
+			}
+			layer.redraw();
+		}
+	}
 
 }
diff --git a/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapPreferences.java b/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapPreferences.java
index f945fac..7f76413 100644
--- a/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapPreferences.java
+++ b/slippymap/src/org/openstreetmap/josm/plugins/slippymap/SlippyMapPreferences.java
@@ -2,7 +2,12 @@ package org.openstreetmap.josm.plugins.slippymap;
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.awt.Image;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -10,6 +15,15 @@ import org.openstreetmap.gui.jmapviewer.OsmTileSource;
 import org.openstreetmap.gui.jmapviewer.OsmTileSource.AbstractOsmTileSource;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
 
 /**
  * Preferences for Slippy Map Tiles
@@ -275,27 +289,168 @@ public class SlippyMapPreferences
         }
     }
 
-    public static class NearMap extends OsmTileSource.AbstractOsmTileSource {
-        public NearMap() {
-            super("NearMap Australia", "http://www.nearmap.com/maps/hl=en&nml=Vert&");
+    public static class BingAerial extends OsmTileSource.AbstractOsmTileSource {
+        private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
+        private static List<Attribution> attributions;
+        
+        public BingAerial() {
+            super("Bing Aerial Maps", "http://ecn.t2.tiles.virtualearth.net/tiles/");
+            
+            attributions = loadAttributionText();
+            System.err.println("Added " + attributions.size() + " attributions.");
+        }
+        
+        class Attribution {
+            String attribution;
+            int minZoom;
+            int maxZoom;
+            Bounds bounds;
+        }
+        
+        class AttrHandler extends DefaultHandler {
+
+            private String string;
+            private Attribution curr;
+            private List<Attribution> attributions = new ArrayList<Attribution>();
+            private double southLat;
+            private double northLat;
+            private double eastLon;
+            private double westLon;
+            private boolean inCoverage = false;
+
+            public void startElement(String uri, String stripped, String tagName,
+                    Attributes attrs) throws SAXException {
+                if("ImageryProvider".equals(tagName)) {
+                    curr = new Attribution();
+                } else if("CoverageArea".equals(tagName)) {
+                    inCoverage = true;
+                }
+            }
+
+            public void characters(char[] ch, int start, int length)
+                    throws SAXException {
+                string = new String(ch, start, length);
+            }
+
+            public void endElement(String uri, String stripped, String tagName)
+                    throws SAXException {
+                if("ImageryProvider".equals(tagName)) {
+                    attributions.add(curr);
+                } else if("Attribution".equals(tagName)) {
+                    curr.attribution = string;
+                } else if(inCoverage && "ZoomMin".equals(tagName)) {
+                    curr.minZoom = Integer.parseInt(string);
+                } else if(inCoverage && "ZoomMax".equals(tagName)) {
+                    curr.maxZoom = Integer.parseInt(string);
+                } else if(inCoverage && "SouthLatitude".equals(tagName)) {
+                    southLat = Double.parseDouble(string);
+                } else if(inCoverage && "NorthLatitude".equals(tagName)) {
+                    northLat = Double.parseDouble(string);
+                } else if(inCoverage && "EastLongitude".equals(tagName)) {
+                    eastLon = Double.parseDouble(string);
+                } else if(inCoverage && "WestLongitude".equals(tagName)) {
+                    westLon = Double.parseDouble(string);
+                } else if("BoundingBox".equals(tagName)) {
+                    curr.bounds = new Bounds(northLat, westLon, southLat, eastLon);
+                } else if("CoverageArea".equals(tagName)) {
+                    inCoverage = false;
+                } 
+                string = "";
+            }
+        }
+
+        private List<Attribution> loadAttributionText() {
+            try {
+                URL u = new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/0,0?zl=1&mapVersion=v1&key="+API_KEY+"&include=ImageryProviders&output=xml");
+                InputStream stream = u.openStream();
+                XMLReader parser = XMLReaderFactory.createXMLReader();
+                AttrHandler handler = new AttrHandler();
+                parser.setContentHandler(handler);
+                parser.parse(new InputSource(stream));
+                return handler.attributions;
+            } catch (IOException e) {
+                System.err.println("Could not open Bing aerials attribution metadata.");
+            } catch (SAXException e) {
+                System.err.println("Could not parse Bing aerials attribution metadata.");
+                e.printStackTrace();
+            }
+            return Collections.emptyList();
         }
 
         @Override
         public int getMaxZoom() {
-            return 21;
+            return 22;
+        }
+        
+        @Override
+        public String getExtension() {
+            return("jpeg");
         }
 
         @Override
         public String getTilePath(int zoom, int tilex, int tiley) {
-            return "z=" + zoom + "&x=" + tilex + "&y=" + tiley;
+            String quadtree = computeQuadTree(zoom, tilex, tiley);
+            return "/tiles/a" + quadtree + "." + getExtension() + "?g=587";
         }
 
         public TileUpdate getTileUpdate() {
             return TileUpdate.IfNoneMatch;
         }
+        
+        public boolean requiresAttribution() {
+            return true;
+        }
+
+        public Image getAttributionImage() {
+            return ImageProvider.get("bing_maps").getImage();
+        }
+        
+        public String getAttributionLinkURL() {
+            //return "http://bing.com/maps"
+            // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU 
+            // (the requirement is that we have such a link at the bottom of the window)
+            return "http://opengeodata.org/microsoft-imagery-details"; 
+        }
+        
+        public String getTermsOfUseURL() {
+            return "http://opengeodata.org/microsoft-imagery-details";
+        }
+        
+        public String getAttributionText(int zoom, LatLon topLeft, LatLon botRight) {
+            Bounds windowBounds = new Bounds(topLeft, botRight);
+            StringBuilder a = new StringBuilder();
+            for (Attribution attr : attributions) {
+                Bounds attrBounds = attr.bounds;
+                if(zoom <= attr.maxZoom && zoom >= attr.minZoom) {
+                    if(windowBounds.getMin().lon() < attrBounds.getMax().lon()
+                            && windowBounds.getMax().lon() > attrBounds.getMin().lon()
+                            && windowBounds.getMax().lat() < attrBounds.getMin().lat()
+                            && windowBounds.getMin().lat() > attrBounds.getMax().lat()) {
+                        a.append(attr.attribution);
+                        a.append(" ");
+                    }
+                }
+            }
+            return a.toString();
+        }
     }
 
-
+    private static String computeQuadTree(int zoom, int tilex, int tiley) {
+        StringBuilder k = new StringBuilder();
+        for(int i = zoom; i > 0; i--) {
+            char digit = 48;
+            int mask = 1 << (i - 1);
+            if ((tilex & mask) != 0) {
+                digit += 1;
+            }
+            if ((tiley & mask) != 0) {
+                digit += 2;
+            }
+            k.append(digit);
+        }
+        return k.toString();
+    }
+    
     public static class HaitiImagery extends OsmTileSource.AbstractOsmTileSource {
         public HaitiImagery() {
             super("HaitiImagery", "http://gravitystorm.dev.openstreetmap.org/imagery/haiti");
@@ -399,10 +554,11 @@ public class SlippyMapPreferences
         sources.add(new OsmTileSource.Mapnik());
         sources.add(new OsmTileSource.CycleMap());
         sources.add(new OsmTileSource.TilesAtHome());
+	// *PLEASE* do not enable BingAerial until we have legal approval.
+        sources.add(new BingAerial());
         sources.add(new Coastline());
         sources.add(new FreeMapySkPokus());
         sources.add(new FreeMapySk());
-        sources.add(new NearMap());
         sources.add(new HaitiImagery());
         sources.addAll(getCustomSources());
         // Probably need to either add these or let users add them somehow
diff --git a/svn-info.xml b/svn-info.xml
index 468d4bf..bb78072 100644
--- a/svn-info.xml
+++ b/svn-info.xml
@@ -3,16 +3,16 @@
 <entry
    kind="dir"
    path="plugins"
-   revision="23541">
+   revision="24608">
 <url>http://svn.openstreetmap.org/applications/editors/josm/plugins</url>
 <repository>
 <root>http://svn.openstreetmap.org</root>
 <uuid>b9d5c4c9-76e1-0310-9c85-f3177eceb1e4</uuid>
 </repository>
 <commit
-   revision="23541">
-<author>upliner</author>
-<date>2010-10-10T18:01:10.304771Z</date>
+   revision="24602">
+<author>malcolmh</author>
+<date>2010-12-05T20:42:28.719021Z</date>
 </commit>
 </entry>
 </info>
diff --git a/wmsplugin/build.xml b/wmsplugin/build.xml
index cd59e06..8f7356d 100644
--- a/wmsplugin/build.xml
+++ b/wmsplugin/build.xml
@@ -27,7 +27,7 @@
 <project name="wmsplugin" default="dist" basedir=".">
 
 
-	<property name="commit.message" value="fixed josm bug 4671 - wms url for sicily has changed" />
+	<property name="commit.message" value="Add getDx() and getDy() methods to WMSLayer to enable other plugins to determine WMS layer shift, see #5565" />
 	<property name="plugin.main.version" value="3530" />
 
 	<property name="josm" location="../../core/dist/josm-custom.jar" />
diff --git a/wmsplugin/sources.cfg b/wmsplugin/sources.cfg
index 6740c95..32c037b 100644
--- a/wmsplugin/sources.cfg
+++ b/wmsplugin/sources.cfg
@@ -1,3 +1,6 @@
+# OUTDATED - only for old plugins
+# See http://josm.openstreetmap.de/wiki/Maps for newer data.
+#
 # FORMAT
 # default(true or false);Name;URL
 # NOTE: default items should be common and worldwide
diff --git a/wmsplugin/src/wmsplugin/AddWMSLayerPanel.java b/wmsplugin/src/wmsplugin/AddWMSLayerPanel.java
index 2722cdf..b6a1a85 100644
--- a/wmsplugin/src/wmsplugin/AddWMSLayerPanel.java
+++ b/wmsplugin/src/wmsplugin/AddWMSLayerPanel.java
@@ -58,469 +58,469 @@ 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() {
-			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() {
-
-			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() {
-			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() {
-				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;
-		}
-	}
+    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() {
+            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() {
+
+            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() {
+            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() {
+                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;
+        }
+    }
 
 }
diff --git a/wmsplugin/src/wmsplugin/WMSLayer.java b/wmsplugin/src/wmsplugin/WMSLayer.java
index 716c2f2..60a9007 100644
--- a/wmsplugin/src/wmsplugin/WMSLayer.java
+++ b/wmsplugin/src/wmsplugin/WMSLayer.java
@@ -264,6 +264,14 @@ public class WMSLayer extends Layer implements PreferenceChangedListener {
         this.dy += dy;
     }
 
+    public double getDx() {
+        return dx;
+    }
+
+    public double getDy() {
+        return dy;
+    }
+
     public int getImageXIndex(double coord) {
         return (int)Math.floor( ((coord - dx) * info.pixelPerDegree) / imageSize);
     }
diff --git a/wmsplugin/src/wmsplugin/WMSLayerInfo.java b/wmsplugin/src/wmsplugin/WMSLayerInfo.java
index 812c60f..519d7d7 100644
--- a/wmsplugin/src/wmsplugin/WMSLayerInfo.java
+++ b/wmsplugin/src/wmsplugin/WMSLayerInfo.java
@@ -20,8 +20,7 @@ import org.openstreetmap.josm.io.MirroredInputStream;
 public class WMSLayerInfo {
     ArrayList<WMSInfo> layers = new ArrayList<WMSInfo>();
     ArrayList<WMSInfo> defaultLayers = new ArrayList<WMSInfo>();
-    private final static String[] DEFAULT_LAYER_SITES = {
-    "http://svn.openstreetmap.org/applications/editors/josm/plugins/wmsplugin/sources.cfg"};
+    private final static String[] DEFAULT_LAYER_SITES = { "http://josm.openstreetmap.de/maps"};
 
     public void load() {
         layers.clear();

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



More information about the Pkg-grass-devel mailing list