Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java	(revision 29371)
@@ -18,5 +18,5 @@
 
     public CalibrationObject(OsmPrimitive object) {
-        this(object, getLastUserId(object));
+        this(object, 0);
     }
 
@@ -29,8 +29,4 @@
     }
     
-    private static long getLastUserId( OsmPrimitive object ) {
-        return object.getUser() == null ? -1 : object.getUser().getId(); // todo?
-    }
-
     @Override
     public void putServerParams( Map<String, String> map ) {
@@ -38,6 +34,9 @@
         map.put("object", object instanceof Node ? "node" : "way");
         map.put("id", String.valueOf(object.getId()));
-        map.put("lastuser", String.valueOf(lastUserId));
     }
-    
+
+    @Override
+    public String toString() {
+        return "CalibrationObject{" + "object=" + object + ", lastUserId=" + lastUserId + "position=" + position + ", date=" + date + ", author=" + author + ", description=" + description + ", abandonDate=" + abandonDate + '}';
+    }
 }
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/DeprecateOffsetAction.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/DeprecateOffsetAction.java	(revision 29371)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/DeprecateOffsetAction.java	(revision 29371)
@@ -0,0 +1,75 @@
+package iodb;
+
+import java.awt.event.ActionEvent;
+import java.io.UnsupportedEncodingException;
+import java.net.*;
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.JosmUserIdentityManager;
+import static org.openstreetmap.josm.tools.I18n.tr;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Download a list of imagery offsets for the current position, let user choose which one to use.
+ * 
+ * @author zverik
+ */
+public class DeprecateOffsetAction extends AbstractAction {
+    private ImageryOffsetBase offset;
+    
+    public DeprecateOffsetAction( ImageryOffsetBase offset ) {
+        super(tr("Deprecate Offset"));
+        putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
+        this.offset = offset;
+        setEnabled(offset != null && !offset.isDeprecated());
+    }
+
+    public void actionPerformed(ActionEvent e) {
+        if( Main.map == null || Main.map.mapView == null || !Main.map.isVisible() )
+            return;
+        
+        if( JOptionPane.showConfirmDialog(Main.parent,
+                tr("Warning: deprecation is irreversible"), // todo: expand
+                ImageryOffsetTools.DIALOG_TITLE, JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION ) {
+            return;
+        }
+        deprecateOffset(offset);
+    }
+
+    public static void deprecateOffset( ImageryOffsetBase offset ) {
+        String userName = JosmUserIdentityManager.getInstance().getUserName();
+        if( userName == null ) {
+            JOptionPane.showMessageDialog(Main.parent, tr("To store imagery offsets you must be a registered OSM user."), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        String message = "Please enter the reason why you mark this "
+                + (offset instanceof ImageryOffset ? "imagery offset" : "calibraion object") + " as deprecated:";
+        String reason = null;
+        boolean iterated = false;
+        while( reason == null ) {
+            reason = JOptionPane.showInputDialog(Main.parent, message, ImageryOffsetTools.DIALOG_TITLE, JOptionPane.PLAIN_MESSAGE);
+            if( reason == null || reason.length() == 0 ) {
+                return;
+            }
+            if( reason.length() < 3 || reason.length() > 200 ) {
+                reason = null;
+                if( !iterated ) {
+                    message = message + "\n" + tr("Reason text should be 3 to 200 letters long.");
+                    iterated = true;
+                }
+            }
+        }
+        
+        try {
+            String query = "deprecate?id=" + offset.getId()
+                + "&author=" + URLEncoder.encode(userName, "UTF8")
+                + "&reason=" + URLEncoder.encode(reason, "UTF8");
+            SimpleOffsetQueryTask depTask = new SimpleOffsetQueryTask(query, tr("Notifying the server of the deprecation..."));
+            Main.worker.submit(depTask);
+        } catch( UnsupportedEncodingException ex ) {
+            // WTF
+        }
+    }
+}
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java	(revision 29371)
@@ -3,28 +3,19 @@
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.*;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.Future;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
+import java.util.*;
+import javax.swing.JOptionPane;
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.AutoScaleAction;
+import org.openstreetmap.josm.actions.DownloadPrimitiveAction;
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.*;
 import org.openstreetmap.josm.data.projection.Projection;
-import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.PleaseWaitRunnable;
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
-import org.openstreetmap.josm.gui.progress.ProgressMonitor;
-import org.openstreetmap.josm.io.OsmTransferException;
 import static org.openstreetmap.josm.tools.I18n.tr;
 import org.openstreetmap.josm.tools.Shortcut;
-import org.xml.sax.SAXException;
 
 /**
@@ -35,13 +26,13 @@
 public class GetImageryOffsetAction extends JosmAction {
     
-    private List<ImageryOffsetBase> offsets;
-    
     public GetImageryOffsetAction() {
         super(tr("Get Imagery Offset..."), "getoffset", tr("Download offsets for current imagery from a server"),
-                Shortcut.registerShortcut("imageryoffset:get", tr("Imagery: {0}", tr("Get Imagery Offset...")), KeyEvent.VK_I, Shortcut.ALT+Shortcut.CTRL), true);
-        offsets = Collections.emptyList();
+                Shortcut.registerShortcut("imageryoffset:get", tr("Imagery: {0}", tr("Get Imagery Offset...")),
+                KeyEvent.VK_I, Shortcut.ALT_CTRL), true);
     }
 
     public void actionPerformed(ActionEvent e) {
+        if( Main.map == null || Main.map.mapView == null || !Main.map.isVisible() )
+            return;
         Projection proj = Main.map.mapView.getProjection();
         LatLon center = proj.eastNorth2latlon(Main.map.mapView.getCenter());
@@ -51,86 +42,121 @@
             return;
         
-        List<ImageryOffsetBase> offsets = download(center, imagery); // todo: async
-        /*DownloadOffsets download = new DownloadOffsets();
-        Future<?> future = Main.worker.submit(download);
-        try {
-            future.get();
-        } catch( Exception ex ) {
-            ex.printStackTrace();
+        DownloadOffsetsTask download = new DownloadOffsetsTask(center, layer, imagery);
+        Main.worker.submit(download);
+    }
+
+    @Override
+    protected void updateEnabledState() {
+        boolean state = true;
+        if( Main.map == null || Main.map.mapView == null || !Main.map.isVisible() )
+            state = false;
+        ImageryLayer layer = ImageryOffsetTools.getTopImageryLayer();
+        if( ImageryOffsetTools.getImageryID(layer) == null )
+            state = false;
+        setEnabled(state);
+    }
+    
+    private void showOffsetDialog( List<ImageryOffsetBase> offsets, ImageryLayer layer ) {
+        if( offsets.isEmpty() ) {
+            JOptionPane.showMessageDialog(Main.parent,
+                    tr("No data for this region. Please adjust imagery layer and upload an offset."),
+                    ImageryOffsetTools.DIALOG_TITLE, JOptionPane.INFORMATION_MESSAGE);
             return;
-        }*/
-        
-        // todo: show a dialog for selecting one of the offsets (without "update" flag)
-        ImageryOffsetBase offset = new OffsetDialog(offsets).showDialog();
+        }
+        final ImageryOffsetBase offset = new OffsetDialog(offsets).showDialog();
         if( offset != null ) {
-            // todo: use the chosen offset
             if( offset instanceof ImageryOffset ) {
                 ImageryOffsetTools.applyLayerOffset(layer, (ImageryOffset)offset);
+                Main.map.repaint();
             } else if( offset instanceof CalibrationObject ) {
-                // todo: select object
+                OsmPrimitive obj = ((CalibrationObject)offset).getObject();
+                final List<PrimitiveId> ids = new ArrayList<PrimitiveId>(1);
+                ids.add(obj);
+                DownloadPrimitiveAction.processItems(false, ids, false, true);
+                Main.worker.submit(new AfterCalibrationDownloadTask((CalibrationObject)offset));
+            }
+        }
+    }
+
+    class AfterCalibrationDownloadTask implements Runnable {
+        private CalibrationObject offset;
+
+        public AfterCalibrationDownloadTask( CalibrationObject offset ) {
+            this.offset = offset;
+        }
+
+        @Override
+        public void run() {
+            OsmPrimitive p = getCurrentDataSet().getPrimitiveById(offset.getObject());
+            if( p == null ) {
+                return;
+            }
+            // check for last user
+            if( offset.getLastUserId() > 0 ) {
+                long uid = p.getUser().getId();
+                Date ts = p.getTimestamp();
+                if( p instanceof Way ) {
+                    for( Node n : ((Way)p).getNodes() ) {
+                        if( n.getTimestamp().after(ts) ) {
+                            ts = n.getTimestamp();
+                            uid = n.getUser().getId();
+                        }
+                    }
+                }
+                if( uid != offset.getLastUserId() ) {
+                    int result = JOptionPane.showConfirmDialog(Main.parent,
+                            tr("The calibration object has been changed in unknown way.\n"
+                             + "It may be moved or extended, thus ceasing to be a reliable mark\n"
+                             + "for imagery calibration. Do you want to notify the server of this?"),
+                            ImageryOffsetTools.DIALOG_TITLE, JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+                    if( result == JOptionPane.YES_OPTION ) {
+                        DeprecateOffsetAction.deprecateOffset(offset);
+                        return;
+                    }
+                }
+            }
+            Main.main.getCurrentDataSet().setSelected(p);
+            AutoScaleAction.zoomTo(Collections.singleton(p));
+            if( !Main.pref.getBoolean("iodb.calibration.message", false) ) {
+                JOptionPane.showMessageDialog(Main.parent,
+                        tr("An object has been selected on the map. Find the corresponding feature\n"
+                         + "on the imagery layer and move that layer accordingly.\n"
+                         + "DO NOT touch the selected object, so it can be used by others later."),
+                        ImageryOffsetTools.DIALOG_TITLE, JOptionPane.INFORMATION_MESSAGE);
+                Main.pref.put("iodb.calibration.message", true);
             }
         }
     }
     
-    private List<ImageryOffsetBase> download( LatLon center, String imagery ) {
-        String base = Main.pref.get("iodb.server.url", "http://offsets.textual.ru/");
-        String query = "get?lat=" + center.getX() + "&lon=" + center.getY();
-        List<ImageryOffsetBase> result = null;
-        try {
-            query = query + "&imagery=" + URLEncoder.encode(imagery, "utf-8");
-            URL url = new URL(base + query);
-            System.out.println("url=" + url);
-            HttpURLConnection connection = (HttpURLConnection)url.openConnection();
-            connection.connect();
-            int retCode = connection.getResponseCode();
-            InputStream inp = connection.getInputStream();
-            if( inp != null ) {
-                result = new IODBReader(inp).parse();
-                System.out.println("result.size() = " + result.size());
+    class DownloadOffsetsTask extends SimpleOffsetQueryTask {
+        private ImageryLayer layer;
+        private List<ImageryOffsetBase> offsets;
+
+        public DownloadOffsetsTask( LatLon center, ImageryLayer layer, String imagery ) {
+            super(null, tr("Loading imagery offsets..."));
+            try {
+                String query = "get?lat=" + center.lat() + "&lon=" + center.lon()
+                        + "&imagery=" + URLEncoder.encode(imagery, "UTF8");
+                setQuery(query);
+            } catch( UnsupportedEncodingException e ) {
+                throw new IllegalArgumentException(e);
             }
-            connection.disconnect();
-        } catch( MalformedURLException ex ) {
-            // ?
-        } catch( UnsupportedEncodingException e ) {
-            // do nothing. WTF is that?
-        } catch( IOException e ) {
-            e.printStackTrace();
-            // ?
-        } catch( SAXException e ) {
-            e.printStackTrace();
-            // ?
-        }
-        if( result == null )
-            result = new ArrayList<ImageryOffsetBase>();
-        return result;
-    }
-    
-    class DownloadOffsets extends PleaseWaitRunnable {
-        
-        private boolean cancelled;
-
-        public DownloadOffsets() {
-            super(tr("Downloading calibration data"));
-            cancelled = false;
+            this.layer = layer;
         }
 
         @Override
-        protected void realRun() throws SAXException, IOException, OsmTransferException {
-            // todo: open httpconnection to server and read xml
-            if( cancelled )
-                return;
-            
-        }
-
-        @Override
-        protected void finish() {
-            if( cancelled )
-                return;
-            // todo: parse xml and return an array of ImageryOffsetBase
+        protected void afterFinish() {
+            if( !cancelled && offsets != null )
+                showOffsetDialog(offsets, layer);
         }
         
         @Override
-        protected void cancel() {
-            cancelled = true;
+        protected void processResponse( InputStream inp ) throws UploadException {
+            offsets = null;
+            try {
+                offsets = new IODBReader(inp).parse();
+            } catch( Exception e ) {
+                throw new UploadException("Error processing XML response: " + e.getMessage());
+            }
         }
     }
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java	(revision 29371)
@@ -59,4 +59,6 @@
                 if( qName.equals("object") ) {
                     fields.isNode = attributes.getValue("type").equals("node");
+                } else if( qName.equals("last-user") ) {
+                    fields.lastUserId = Integer.parseInt(attributes.getValue("id"));
                 } else if( qName.equals("imagery-position") ) {
                     fields.imageryPos = parseLatLon(attributes);
@@ -102,6 +104,4 @@
                 } else if( qName.equals("object") ) {
                     fields.objectId = Integer.parseInt(accumulator.toString());
-                } else if( qName.equals("last-user") ) {
-                    fields.lastUserId = Integer.parseInt(accumulator.toString());
                 } else if( qName.equals("offset") || qName.equals("calibration-object") ) {
                     // store offset
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java	(revision 29371)
@@ -57,3 +57,8 @@
             map.put("maxzoom", String.valueOf(maxZoom));
     }
+
+    @Override
+    public String toString() {
+        return "ImageryOffset{" + "imageryPos=" + imageryPos + ", imagery=" + imagery + "position=" + position + ", date=" + date + ", author=" + author + ", description=" + description + ", abandonDate=" + abandonDate + '}';
+    }
 }
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java	(revision 29371)
@@ -12,9 +12,12 @@
  */
 public class ImageryOffsetBase {
-    private LatLon position;
-    private Date date;
-    private String author;
-    private String description;
-    private Date abandonDate;
+    protected long offsetId;
+    protected LatLon position;
+    protected Date date;
+    protected String author;
+    protected String description;
+    protected Date abandonDate;
+    protected String abandonAuthor;
+    protected String abandonReason;
     
     public void setBasicInfo( LatLon position, String author, String description, Date date ) {
@@ -26,4 +29,12 @@
     }
 
+    public void setId( long id ) {
+        this.offsetId = id;
+    }
+
+    public long getId() {
+        return offsetId;
+    }
+
     public void setAbandonDate(Date abandonDate) {
         this.abandonDate = abandonDate;
@@ -33,6 +44,14 @@
         return abandonDate;
     }
+
+    public String getAbandonAuthor() {
+        return abandonAuthor;
+    }
+
+    public String getAbandonReason() {
+        return abandonReason;
+    }
     
-    public boolean isAbandoned() {
+    public boolean isDeprecated() {
         return abandonDate != null;
     }
@@ -50,4 +69,8 @@
     }
 
+    public void setDescription( String description ) {
+        this.description = description;
+    }
+
     public LatLon getPosition() {
         return position;
@@ -60,3 +83,8 @@
         map.put("description", description);
     }
+
+    @Override
+    public String toString() {
+        return "ImageryOffsetBase{" + "position=" + position + ", date=" + date + ", author=" + author + ", description=" + description + ", abandonDate=" + abandonDate + '}';
+    }
 }
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java	(revision 29371)
@@ -20,9 +20,11 @@
         storeAction = new StoreImageryOffsetAction();
         
-        Main.main.menu.imageryMenu.addSeparator();
-        Main.main.menu.imageryMenu.add(getAction);
-        Main.main.menu.imageryMenu.add(storeAction);
-        
-        // todo: make MapMode for viewing and updating imagery offsets
+        // todo: correct menu
+        Main.main.menu.viewMenu.addSeparator();
+        Main.main.menu.viewMenu.add(getAction);
+        Main.main.menu.viewMenu.add(storeAction);
+
+        // todo: add a button on toolbar
+        // todo: make MapMode for viewing and updating imagery offsets (is it needed?)
     }
 }
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetTools.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetTools.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetTools.java	(revision 29371)
@@ -1,12 +1,13 @@
 package iodb;
 
-import java.util.HashMap;
-import java.util.List;
+import java.util.*;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import static org.openstreetmap.josm.tools.I18n.tr;
 
 /**
@@ -16,7 +17,9 @@
  */
 public class ImageryOffsetTools {
-    private static HashMap<String, String> imageryAliases;
+    public static final String DIALOG_TITLE = tr("Imagery Offset");
     
     public static ImageryLayer getTopImageryLayer() {
+        if( Main.map == null || Main.map.mapView == null )
+            return null;
         List<ImageryLayer> layers = Main.map.mapView.getLayersOfType(ImageryLayer.class);
         for( ImageryLayer layer : layers ) {
@@ -28,5 +31,5 @@
     }
     
-    private static LatLon getMapCenter() {
+    public static LatLon getMapCenter() {
         Projection proj = Main.getProjection();
         return Main.map == null || Main.map.mapView == null
@@ -37,5 +40,5 @@
         Projection proj = Main.getProjection();
         EastNorth offsetCenter = proj.latlon2eastNorth(center);
-        EastNorth centerOffset = offsetCenter.add(layer.getDx(), layer.getDy()); // todo: add or substract?
+        EastNorth centerOffset = offsetCenter.add(-layer.getDx(), -layer.getDy()); // todo: add or substract?
         LatLon offsetLL = proj.eastNorth2latlon(centerOffset);
         return offsetLL;
@@ -46,5 +49,5 @@
         EastNorth center = proj.latlon2eastNorth(offset.getPosition());
         EastNorth offsetPos = proj.latlon2eastNorth(offset.getImageryPos());
-        layer.setOffset(offsetPos.getX() - center.getX(), offsetPos.getY() - center.getY()); // todo: + or -?
+        layer.setOffset(center.getX() - offsetPos.getX(), center.getY() - offsetPos.getY());
     }
     
@@ -57,22 +60,64 @@
             return null;
         
-        if( imageryAliases == null )
-            loadImageryAliases();
-        for( String substr : imageryAliases.keySet() )
-            if( url.contains(substr) )
-                return imageryAliases.get(substr);
-        
-        return url; // todo: strip parametric parts, etc
-    }
-    
-    private static void loadImageryAliases() {
-        if( imageryAliases == null )
-            imageryAliases = new HashMap<String, String>();
-        else
-            imageryAliases.clear();
-        
-        // { substring, alias }
-        imageryAliases.put("bing", "bing");
-        // todo: load from a resource?
+        // predefined layers
+        if( layer.getInfo().getImageryType().equals(ImageryInfo.ImageryType.BING) || url.contains("tiles.virtualearth.net") )
+            return "bing";
+
+        if( layer.getInfo().getImageryType().equals(ImageryInfo.ImageryType.SCANEX) && url.toLowerCase().equals("irs") )
+            return "scanex_irs";
+
+        boolean isWMS = layer.getInfo().getImageryType().equals(ImageryInfo.ImageryType.WMS);
+
+        System.out.println(url);
+
+        // Remove protocol
+        int i = url.indexOf("://");
+        url = url.substring(i + 3);
+
+        // Split URL into address and query string
+        i = url.indexOf('?');
+        String query = "";
+        if( i > 0 ) {
+            query = url.substring(i);
+            url = url.substring(0, i);
+        }
+
+        // Parse query parameters into a sorted map
+        Map<String, String> qparams = new TreeMap<String, String>();
+        String[] qparamsStr = query.length() > 1 ? query.substring(1).split("&") : new String[0];
+        for( String param : qparamsStr ) {
+            String[] kv = param.split("=");
+            kv[0] = kv[0].toLowerCase();
+            // WMS: if this is WMS, remove all parameters except map and layers
+            if( isWMS && !(kv[0].equals("map") || kv[0].equals("layers")) )
+                continue;
+            // TMS: skip parameters with variable values
+            if( kv.length > 1 && kv[1].indexOf('{') >= 0 && kv[1].indexOf('}') > 0 )
+                continue;
+            qparams.put(kv[0].toLowerCase(), kv.length > 1 ? kv[1] : null);
+        }
+
+        // Reconstruct query parameters
+        StringBuilder sb = new StringBuilder();
+        for( String qk : qparams.keySet() ) {
+            if( sb.length() > 0 )
+                sb.append('&');
+            else if( query.length() > 0 )
+                sb.append('?');
+            sb.append(qk).append('=').append(qparams.get(qk));
+        }
+        query = sb.toString();
+
+        // TMS: remove /{zoom} and /{y}.png parts
+        url = url.replaceAll("\\/\\{[^}]+\\}(?:\\.\\w+)?", "");
+        // TMS: remove variable parts
+        url = url.replaceAll("\\{[^}]+\\}", "");
+        while( url.contains("..") )
+            url = url.replace("..", ".");
+        if( url.startsWith(".") )
+            url = url.substring(1);
+
+        System.out.println("-> " + url + query);
+        return url + query;
     }
     
@@ -110,3 +155,7 @@
         return intResult;
     }
+
+    public static String getServerURL() {
+        return Main.pref.get("iodb.server.url", "http://offsets.textual.ru/");
+    }
 }
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialog.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialog.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialog.java	(revision 29371)
@@ -29,6 +29,12 @@
             OffsetDialogButton button = new OffsetDialogButton(offset);
             button.addActionListener(this);
+/*            JPopupMenu popupMenu = new JPopupMenu();
+            popupMenu.add(new OffsetInfoAction(offset));
+            if( !offset.isDeprecated() )
+                popupMenu.add(new DeprecateOffsetAction(offset));
+            button.add(popupMenu);*/
             buttonPanel.add(button);
         }
+        // todo: calibration objects and deprecated offsets button
         JButton cancelButton = new JButton("Cancel");
         cancelButton.addActionListener(this);
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java	(revision 29371)
@@ -13,5 +13,5 @@
 
     public OffsetDialogButton( ImageryOffsetBase offset ) {
-        super(offset.getDescription() + " (" + offset.getPosition().lat() + ", " + offset.getPosition().lon() + ")");
+        super(offset.getDescription() + " (" + Math.round(offset.getPosition().greatCircleDistance(ImageryOffsetTools.getMapCenter())) + " m)");
         this.offset = offset;
     }
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetInfoAction.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetInfoAction.java	(revision 29371)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetInfoAction.java	(revision 29371)
@@ -0,0 +1,28 @@
+package iodb;
+
+import java.awt.event.ActionEvent;
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+import org.openstreetmap.josm.Main;
+import static org.openstreetmap.josm.tools.I18n.tr;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Download a list of imagery offsets for the current position, let user choose which one to use.
+ * 
+ * @author zverik
+ */
+public class OffsetInfoAction extends AbstractAction {
+    private ImageryOffsetBase offset;
+    
+    public OffsetInfoAction( ImageryOffsetBase offset ) {
+        super(tr("Offset Information"));
+        putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
+        this.offset = offset;
+        setEnabled(offset != null);
+    }
+
+    public void actionPerformed(ActionEvent e) {
+        JOptionPane.showMessageDialog(Main.parent, "TODO", ImageryOffsetTools.DIALOG_TITLE, JOptionPane.PLAIN_MESSAGE);
+    }
+}
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/StoreImageryOffsetAction.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/StoreImageryOffsetAction.java	(revision 29369)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/StoreImageryOffsetAction.java	(revision 29371)
@@ -2,10 +2,14 @@
 
 import java.awt.event.ActionEvent;
-import java.util.HashMap;
-import java.util.Map;
+import java.io.UnsupportedEncodingException;
+import java.net.*;
+import java.util.*;
+import javax.swing.JOptionPane;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.osm.*;
+import org.openstreetmap.josm.gui.JosmUserIdentityManager;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import static org.openstreetmap.josm.tools.I18n.tr;
 
@@ -18,23 +22,126 @@
 
     public StoreImageryOffsetAction() {
-        super(tr("Store Imagery Offset..."), "storeoffset", tr("Upload an offset for current imagery (or calibration object information) to a server"), null, false);
+        super(tr("Store Imagery Offset..."), "storeoffset",
+                tr("Upload an offset for current imagery (or calibration object information) to a server"),
+                null, false);
     }
 
     public void actionPerformed(ActionEvent e) {
-        // todo: check that there is an imagery
-        // and that it is moved
-        Projection proj = Main.map.mapView.getProjection();
-        LatLon center = proj.eastNorth2latlon(Main.map.mapView.getCenter());
-        // todo: open an upload window
-        // todo: if an object was selected, ask if the user wants to upload it
-        // todo: enter all metadata (that is, a description)
-        // todo: upload object info to server
+        if( Main.map == null || Main.map.mapView == null || getCurrentDataSet() == null )
+            return;
+
+        ImageryLayer layer = ImageryOffsetTools.getTopImageryLayer();
+        if( layer == null )
+            return;
+
+        String userName = JosmUserIdentityManager.getInstance().getUserName();
+        if( userName == null ) {
+            JOptionPane.showMessageDialog(Main.parent, tr("To store imagery offsets you must be a registered OSM user."), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+            
+        // check if an object suitable for calibration is selected
+        OsmPrimitive calibration = null;
+        Collection<OsmPrimitive> selectedObjects = getCurrentDataSet().getSelected();
+        if( selectedObjects.size() == 1 ) {
+            OsmPrimitive selection = selectedObjects.iterator().next();
+            if( selection instanceof Node || selection instanceof Way ) {
+                boolean suitable = !selection.isNewOrUndeleted() && !selection.isDeleted() && !selection.isModified();
+                if( selection instanceof Way ) {
+                    for( Node n : ((Way)selection).getNodes() )
+                        if( n.isNewOrUndeleted() || n.isDeleted() || n.isModified() )
+                            suitable = false;
+                } else if( selection.isReferredByWays(1) ) {
+                    suitable = false;
+                }
+                if( suitable ) {
+                    String[] options = new String[] {tr("Store calibration object"), tr("Store imagery offset"), tr("Cancel")};
+                    int result = JOptionPane.showOptionDialog(Main.parent,
+                            tr("The selected object can be used as a calibration object. What do you intend to do?"), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE,
+                            null, options, options[0]);
+                    if( result == 2 || result == JOptionPane.CLOSED_OPTION )
+                        return;
+                    if( result == 0 )
+                        calibration = selection;
+                } else {
+                    String[] options = new String[] {tr("Store imagery offset"), tr("Cancel")};
+                    int result = JOptionPane.showOptionDialog(Main.parent,
+                            tr("You have an object selected and might want to use it as a calibration object.\n"
+                             + "But in this case it should be uploaded to OSM server first."), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
+                            null, options, options[1]);
+                    if( result == 1 || result == JOptionPane.CLOSED_OPTION )
+                        return;
+                }
+            }
+        }
+
+        Object message = "";
+        LatLon center = ImageryOffsetTools.getMapCenter();
+        ImageryOffsetBase offsetObj;
+        if( calibration == null ) {
+            // register imagery offset
+            if( Math.abs(layer.getDx()) < 1e-8 && Math.abs(layer.getDy()) < 1e-8 ) {
+                if( JOptionPane.showConfirmDialog(Main.parent,
+                        tr("The topmost imagery layer has no offset. Are you sure you want to upload it?"), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION )
+                    return;
+            }
+            LatLon offset = ImageryOffsetTools.getLayerOffset(layer, center);
+            offsetObj = new ImageryOffset(ImageryOffsetTools.getImageryID(layer), offset);
+            message = "You are registering an imagery offset.\n"
+                    + "Other users in this area will be able to use it for mapping.\n"
+                    + "Please make sure it is as precise as possible, and\n"
+                    + "describe a region this offset is applicable to.";
+        } else {
+            // register calibration object
+            offsetObj = new CalibrationObject(calibration);
+            message = "You are registering calibration object.\n"
+                    + "It should be the most precisely positioned object,\n"
+                    + "with clearly visible boundaries on various satellite imagery.\n"
+                    + "Please describe a region where this object is located.";
+        }
+        offsetObj.setBasicInfo(center, userName, null, null);
+        String description = null;
+        boolean iterated = false;
+        while( description == null ) {
+            description = JOptionPane.showInputDialog(Main.parent, message, ImageryOffsetTools.DIALOG_TITLE, JOptionPane.PLAIN_MESSAGE);
+            if( description == null || description.length() == 0 )
+                return;
+            if( description.length() < 3 || description.length() > 200 ) {
+                description = null;
+                if( !iterated ) {
+                    message = message + "\n" + tr("Description should be 3 to 200 letters long.");
+                    iterated = true;
+                }
+            }
+        }
+        offsetObj.setDescription(description);
+
+        // upload object info to server
+        try {
+            Map<String, String> params = new HashMap<String, String>();
+            offsetObj.putServerParams(params);
+            StringBuilder query = null;
+            for( String key : params.keySet() ) {
+                if( query == null ) {
+                    query = new StringBuilder("store?");
+                } else {
+                    query.append('&');
+                }
+                query.append(key).append('=').append(URLEncoder.encode(params.get(key), "UTF8"));
+            }
+            Main.main.worker.submit(new SimpleOffsetQueryTask(query.toString(), tr("Uploading the new offset...")));
+        } catch( UnsupportedEncodingException ex ) {
+            // WTF
+        }
     }
-    
-    private static void upload( ImageryOffsetBase offset ) {
-        String base = Main.pref.get("iodb.server.url", "http://offsets.textual.ru/");
-        Map<String, String> params = new HashMap<String, String>();
-        offset.putServerParams(params);
-        // todo
+
+    @Override
+    protected void updateEnabledState() {
+        boolean state = true;
+        if( Main.map == null || Main.map.mapView == null || !Main.map.isVisible() )
+            state = false;
+        if( ImageryOffsetTools.getTopImageryLayer() == null )
+            state = false;
+        setEnabled(state);
     }
 }
