Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationLayer.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationLayer.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationLayer.java	(revision 29384)
@@ -20,5 +20,6 @@
  * A layer that displays calibration geometry for an offset.
  *
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class CalibrationLayer extends Layer {
@@ -34,4 +35,8 @@
     }
 
+    /**
+     * Draw the calibration geometry with thin bright lines (or a crosshair
+     * in case of a point).
+     */
     @Override
     public void paint( Graphics2D g, MapView mv, Bounds box ) {
@@ -79,4 +84,7 @@
     }
 
+    /**
+     * This is for determining a bounding box for the layer.
+     */
     @Override
     public void visitBoundingBox( BoundingXYVisitor v ) {
@@ -85,8 +93,11 @@
     }
 
+    /**
+     * A simple tooltip with geometry type, status and author.
+     */
     @Override
     public String getToolTipText() {
-        return "A " + (obj.isDeprecated() ? "deprecated " : "") + "calibration " + OffsetInfoAction.getGeometryType(obj)
-                + " by " + obj.getAuthor();
+        return "A " + (obj.isDeprecated() ? "deprecated " : "") + "calibration "
+                + OffsetInfoAction.getGeometryType(obj) + " by " + obj.getAuthor();
     }
 
@@ -96,4 +107,7 @@
     }
 
+    /**
+     * This method returns standard actions plus "zoom to layer" and "change color".
+     */
     @Override
     public Action[] getMenuEntries() {
@@ -111,4 +125,9 @@
     }
 
+    /**
+     * This method pans to the geometry, preserving zoom. It is used
+     * from {@link GetImageryOffsetAction}, because {@link AutoScaleAction}
+     * doesn't have a relevant method.
+     */
     public void panToCenter() {
         if( center == null ) {
@@ -125,4 +144,9 @@
     }
 
+    /**
+     * An action to change a color of a geometry. The color
+     * is specified in the constuctor. See {@link #getMenuEntries()} for
+     * the list of enabled colors.
+     */
     class SelectColorAction extends AbstractAction {
         private Color c;
@@ -140,4 +164,7 @@
     }
 
+    /**
+     * A simple icon with a colored rectangle.
+     */
     class SingleColorIcon implements Icon {
         private Color color;
@@ -162,7 +189,12 @@
     }
 
+    /**
+     * An action that calls {@link AutoScaleAction} which in turn
+     * uses {@link #visitBoundingBox} to pan and zoom to the calibration geometry.
+     */
     class ZoomToLayerAction extends AbstractAction {
         public ZoomToLayerAction() {
             super(tr("Zoom to layer"));
+            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale/layer"));
         }
 
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java	(revision 29384)
@@ -8,14 +8,24 @@
 
 /**
+ * A calibration geometry data type. It was called an object long ago,
+ * when it contained an information on an OSM object. I decided not to rename
+ * this class.
  *
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class CalibrationObject extends ImageryOffsetBase {
     private LatLon[] geometry;
 
+    /**
+     * Initialize a calibration object from the array of nodes.
+     */
     public CalibrationObject(LatLon[] geometry) {
         this.geometry = geometry;
     }
 
+    /**
+     * Initialize a calibration object from OSM primitive.
+     */
     public CalibrationObject( OsmPrimitive p ) {
         if( p instanceof Node )
@@ -29,4 +39,7 @@
     }
 
+    /**
+     * Get an array of points for this geometry.
+     */
     public LatLon[] getGeometry() {
         return geometry;
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/DeprecateOffsetAction.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/DeprecateOffsetAction.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/DeprecateOffsetAction.java	(revision 29384)
@@ -12,7 +12,8 @@
 
 /**
- * Download a list of imagery offsets for the current position, let user choose which one to use.
+ * A context-dependent action to deprecate an offset.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class DeprecateOffsetAction extends AbstractAction {
@@ -20,4 +21,7 @@
     private QuerySuccessListener listener;
     
+    /**
+     * Initialize an action with an offset object.
+     */
     public DeprecateOffsetAction( ImageryOffsetBase offset ) {
         super(tr("Deprecate Offset"));
@@ -27,4 +31,10 @@
     }
 
+    /**
+     * Asks a user if they really want to deprecate an offset (since this
+     * action is virtually irreversible) and calls
+     * {@link #deprecateOffset(iodb.ImageryOffsetBase, iodb.QuerySuccessListener)}
+     * on a positive answer.
+     */
     public void actionPerformed(ActionEvent e) {
         if( Main.map == null || Main.map.mapView == null || !Main.map.isVisible() )
@@ -41,12 +51,23 @@
     }
 
+    /**
+     * Installs a listener to process successful deprecation event.
+     */
     public void setListener( QuerySuccessListener listener ) {
         this.listener = listener;
     }
 
+    /**
+     * Deprecate the given offset.
+     * @see #deprecateOffset(iodb.ImageryOffsetBase, iodb.QuerySuccessListener) 
+     */
     public static void deprecateOffset( ImageryOffsetBase offset ) {
         deprecateOffset(offset, null);
     }
 
+    /**
+     * Deprecate the given offset and call listener on success. Asks user the reason
+     * and executes {@link SimpleOffsetQueryTask} with a query to deprecate the offset.
+     */
     public static void deprecateOffset( ImageryOffsetBase offset, QuerySuccessListener listener ) {
         String userName = JosmUserIdentityManager.getInstance().getUserName();
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java	(revision 29384)
@@ -20,8 +20,12 @@
  * Download a list of imagery offsets for the current position, let user choose which one to use.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class GetImageryOffsetAction extends JosmAction {
     
+    /**
+     * Initialize the action. Sets "Ctrl+Alt+I" shortcut: the only shortcut in this plugin.
+     */
     public GetImageryOffsetAction() {
         super(tr("Get Imagery Offset..."), "getoffset", tr("Download offsets for current imagery from a server"),
@@ -30,4 +34,7 @@
     }
 
+    /**
+     * The action just executes {@link DownloadOffsetsTask}.
+     */
     public void actionPerformed(ActionEvent e) {
         if( Main.map == null || Main.map.mapView == null || !Main.map.isVisible() )
@@ -44,4 +51,8 @@
     }
 
+    /**
+     * This action is enabled when there's a map, mapView and one of the layers
+     * is an imagery layer.
+     */
     @Override
     protected void updateEnabledState() {
@@ -55,5 +66,10 @@
     }
     
-    private void showOffsetDialog( List<ImageryOffsetBase> offsets, ImageryLayer layer ) {
+    /**
+     * Display a dialog for choosing between offsets. If there are no offsets in
+     * the list, displays the relevant message instead.
+     * @param offsets List of offset objects to choose from.
+     */
+    private void showOffsetDialog( List<ImageryOffsetBase> offsets ) {
         if( offsets.isEmpty() ) {
             JOptionPane.showMessageDialog(Main.parent,
@@ -67,8 +83,19 @@
     }
 
+    /**
+     * A task that downloads offsets for a given position and imagery layer,
+     * then parses resulting XML and calls
+     * {@link #showOffsetDialog(java.util.List)} on success.
+     */
     class DownloadOffsetsTask extends SimpleOffsetQueryTask {
         private ImageryLayer layer;
         private List<ImageryOffsetBase> offsets;
 
+        /**
+         * Initializes query object from the parameters.
+         * @param center A center point of a map view.
+         * @param layer The topmost imagery layer.
+         * @param imagery Imagery ID for the layer.
+         */
         public DownloadOffsetsTask( LatLon center, ImageryLayer layer, String imagery ) {
             super(null, tr("Loading imagery offsets..."));
@@ -87,10 +114,18 @@
         }
 
+        /**
+         * Displays offset dialog on success.
+         */
         @Override
         protected void afterFinish() {
             if( !cancelled && offsets != null )
-                showOffsetDialog(offsets, layer);
+                showOffsetDialog(offsets);
         }
         
+        /**
+         * Parses the response with {@link IODBReader}.
+         * @param inp Response input stream.
+         * @throws iodb.SimpleOffsetQueryTask.UploadException Thrown on XML parsing error.
+         */
         @Override
         protected void processResponse( InputStream inp ) throws UploadException {
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java	(revision 29384)
@@ -18,7 +18,9 @@
 
 /**
- * Parses the message from server. It expects XML in UTF-8 with several &lt;offset&gt; elements.
+ * Parses the server response. It expects XML in UTF-8 with several &lt;offset&gt;
+ * and &lt;calibration&gt; elements.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class IODBReader {
@@ -26,4 +28,37 @@
     private InputSource source;
     
+    /**
+     * Initializes the parser. This constructor creates an input source on the input
+     * stream, so it may throw an exception (though it's highly improbable).
+     * @param source An input stream with XML.
+     * @throws IOException Thrown when something's wrong with the stream.
+     */
+    public IODBReader( InputStream source ) throws IOException {
+        this.source = new InputSource(UTFInputStreamReader.create(source, "UTF-8"));
+        this.offsets = new ArrayList<ImageryOffsetBase>();
+    }
+
+    /**
+     * Parses the XML input stream. Creates {@link Parser} to do it.
+     * @return The list of offsets.
+     * @throws SAXException Thrown when the XML is malformed.
+     * @throws IOException Thrown when the input stream fails.
+     */
+    public List<ImageryOffsetBase> parse() throws SAXException, IOException {
+        Parser parser = new Parser();
+        try {
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            factory.newSAXParser().parse(source, parser);
+            return offsets;
+        } catch( ParserConfigurationException e ) {
+            throw new SAXException(e);
+        }
+    }
+
+    /**
+     * The SAX handler for XML from the imagery offset server.
+     * Calls {@link IOFields#constructObject()} for every complete object
+     * and appends the result to offsets array.
+     */
     private class Parser extends DefaultHandler {
         private StringBuffer accumulator = new StringBuffer();
@@ -33,4 +68,7 @@
         private SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd");
 
+        /**
+         * Initialize all fields.
+         */
         @Override
         public void startDocument() throws SAXException {
@@ -40,4 +78,11 @@
         }
 
+        /**
+         * Parses latitude and longitude from tag attributes.
+         * It expects to find them in "lat" and "lon" attributes
+         * as decimal degrees. Note that it does not check whether
+         * the resulting object is valid: it may not be, especially
+         * for locations near the Poles and 180th meridian.
+         */
         private LatLon parseLatLon(Attributes atts) {
             return new LatLon(
@@ -111,4 +156,6 @@
                         offsets.add(fields.constructObject());
                     } catch( IllegalArgumentException ex ) {
+                        // On one hand, we don't care, but this situation is one
+                        // of those "it can never happen" cases.
                         System.err.println(ex.getMessage());
                     }
@@ -119,22 +166,9 @@
     }
     
-
-    public IODBReader( InputStream source ) throws IOException {
-        this.source = new InputSource(UTFInputStreamReader.create(source, "UTF-8"));
-        this.offsets = new ArrayList<ImageryOffsetBase>();
-    }
-    
-    public List<ImageryOffsetBase> parse() throws SAXException, IOException {
-        Parser parser = new Parser();
-        try {
-            SAXParserFactory factory = SAXParserFactory.newInstance();
-            factory.newSAXParser().parse(source, parser);
-            return offsets;
-        } catch (ParserConfigurationException e) {
-            e.printStackTrace();
-            throw new SAXException(e);
-        }
-    }
-    
+    /**
+     * An accumulator for parsed fields. When there's enough data, it can construct
+     * an offset object. All fields are public to deliver us from tons of getters
+     * and setters.
+     */
     private class IOFields {
         public int id;
@@ -151,8 +185,14 @@
         public List<LatLon> geometry;
 
+        /**
+         * A constructor just calls {@link #clear()}.
+         */
         public IOFields() {
             clear();
         }
         
+        /**
+         * Clear all fields to <tt>null</tt> and <tt>-1</tt>.
+         */
         public void clear() {
             id = -1;
@@ -171,4 +211,8 @@
         }
 
+        /**
+         * Creates an offset object from the fields. Also validates them, but not vigorously.
+         * @return A new offset object.
+         */
         public ImageryOffsetBase constructObject() {
             if( author == null || description == null || position == null || date == null )
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryIdGenerator.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryIdGenerator.java	(revision 29384)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryIdGenerator.java	(revision 29384)
@@ -0,0 +1,82 @@
+package iodb;
+
+import java.util.*;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
+
+/**
+ * Generate unique imagery identifier based on its type and URL.
+ *
+ * @author Zverik
+ * @license WTFPL
+ */
+public class ImageryIdGenerator {
+
+    public static String getImageryID( String url, ImageryType type ) {
+        if( url == null )
+            return null;
+
+        // predefined layers
+        if( ImageryType.BING.equals(type) || url.contains("tiles.virtualearth.net") )
+            return "bing";
+
+        if( ImageryType.SCANEX.equals(type) && url.toLowerCase().equals("irs") )
+            return "scanex_irs";
+
+        boolean isWMS = ImageryType.WMS.equals(type);
+
+//        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
+        final Set<String> removeWMSParams = new TreeSet<String>(Arrays.asList(new String[] {
+                    "srs", "width", "height", "bbox", "service", "request", "version", "format", "styles", "transparent"
+                }));
+        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 && removeWMSParams.contains(kv[0]) )
+                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;
+    }
+}
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java	(revision 29384)
@@ -6,7 +6,10 @@
 
 /**
- * An offset.
+ * An imagery offset. Contains imagery identifier, zoom bracket and a location
+ * of the position point on the imagery layer. The offset is then calculated
+ * as a difference between the two.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class ImageryOffset extends ImageryOffsetBase {
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java	(revision 29384)
@@ -7,7 +7,10 @@
 
 /**
- * Stores one imagery offset record.
+ * Stores one offset record. It is the superclass for {@link ImageryOffset}
+ * and {@link CalibrationObject} classes and contains common fields
+ * like position, author and description.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class ImageryOffsetBase {
@@ -21,4 +24,8 @@
     protected String abandonReason;
     
+    /**
+     * Initialize object with the basic information. It's offset location, author, date
+     * and description.
+     */
     public void setBasicInfo( LatLon position, String author, String description, Date date ) {
         this.position = position;
@@ -37,4 +44,8 @@
     }
 
+    /**
+     * Mark the offset as deprecated. Though there is no exact field for "isDeprecated",
+     * it is deduced from abandonDate, author and reason being not null.
+     */
     public void setDeprecated(Date abandonDate, String author, String reason) {
         this.abandonDate = abandonDate;
@@ -55,4 +66,8 @@
     }
     
+    /**
+     * Check that {@link #getAbandonDate()} is not null. Note that
+     * is doesn't say anything about abandonAuthor or abandonReason.
+     */
     public boolean isDeprecated() {
         return abandonDate != null;
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java	(revision 29384)
@@ -9,7 +9,8 @@
 
 /**
- * Add some actions to the imagery menu.
+ * A plugin to request and store imagery offsets in the centralized database.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class ImageryOffsetPlugin extends Plugin {
@@ -17,4 +18,10 @@
     private StoreImageryOffsetAction storeAction;
     
+    /**
+     * Add both actions to their own menu. This creates
+     * "Offset" menu, because "Imagery" is constantly rebuilt,
+     * losing all changes, and other menus are either too long already,
+     * or completely unsuitable for imagery offset actions.
+     */
     public ImageryOffsetPlugin( PluginInformation info ) {
         super(info);
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetTools.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetTools.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetTools.java	(revision 29384)
@@ -6,5 +6,4 @@
 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;
@@ -13,11 +12,19 @@
 
 /**
- * Some common static methods for querying imagery layers.
+ * Some common static methods for querying and processing imagery layers.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class ImageryOffsetTools {
+    /**
+     * A title for all dialogs created in this plugin.
+     */
     public static final String DIALOG_TITLE = tr("Imagery Offset");
     
+    /**
+     * Returns the topmost visible imagery layer.
+     * @return the layer, or null if it hasn't been found.
+     */
     public static ImageryLayer getTopImageryLayer() {
         if( Main.map == null || Main.map.mapView == null )
@@ -32,4 +39,8 @@
     }
     
+    /**
+     * Calculates the center of a visible map area.
+     * @return the center point, or (0; 0) if there's no map on the screen.
+     */
     public static LatLon getMapCenter() {
         Projection proj = Main.getProjection();
@@ -38,4 +49,11 @@
     }
     
+    /**
+     * Calculates an imagery layer offset.
+     * @param center The center of a visible map area.
+     * @return Coordinates of a point on the imagery which correspond to the
+     * center point on the map.
+     * @see #applyLayerOffset
+     */
     public static LatLon getLayerOffset( ImageryLayer layer, LatLon center ) {
         Projection proj = Main.getProjection();
@@ -46,4 +64,9 @@
     }
     
+    /**
+     * Applies the offset to the imagery layer.
+     * @see #calculateOffset(iodb.ImageryOffset)
+     * @see #getLayerOffset
+     */
     public static void applyLayerOffset( ImageryLayer layer, ImageryOffset offset ) {
         double[] dxy = calculateOffset(offset);
@@ -53,5 +76,6 @@
     /**
      * Calculate dx and dy for imagery offset.
-     * @return [dx, dy]
+     * @return An array of [dx, dy].
+     * @see #applyLayerOffset
      */
     public static double[] calculateOffset( ImageryOffset offset ) {
@@ -62,77 +86,14 @@
     }
     
+    /**
+     * Generate unique imagery identifier based on its type and URL.
+     * @param layer imagery layer.
+     * @return imagery id.
+     */
     public static String getImageryID( ImageryLayer layer ) {
-        if( layer == null )
-            return null;
-        
-        String url = layer.getInfo().getUrl();
-        if( url == null )
-            return null;
-        
-        // predefined layers
-        if( layer.getInfo().getImageryType().equals(ImageryInfo.ImageryType.BING) || url.contains("tiles.virtualearth.net") )
-            return "bing";
+        return layer == null ? null :
+                ImageryIdGenerator.getImageryID(layer.getInfo().getUrl(), layer.getInfo().getImageryType());
+    }
 
-        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
-        final Set<String> removeWMSParams = new TreeSet<String>(Arrays.asList(new String[] {
-            "srs", "width", "height", "bbox", "service", "request", "version", "format", "styles", "transparent"
-        }));
-        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 && removeWMSParams.contains(kv[0]) )
-                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;
-    }
-    
     // Following three methods were snatched from TMSLayer
     private static double latToTileY(double lat, int zoom) {
@@ -169,23 +130,9 @@
     }
 
-    public static double[] getLengthAndDirection( ImageryOffset offset ) {
-        return getLengthAndDirection(offset, 0.0, 0.0);
-    }
-
-    public static double[] getLengthAndDirection( ImageryOffset offset, double dx, double dy ) {
-        Projection proj = Main.getProjection();
-        EastNorth pos = proj.latlon2eastNorth(offset.getPosition());
-        LatLon correctedCenterLL = proj.eastNorth2latlon(pos.add(-dx, -dy));
-        double length = correctedCenterLL.greatCircleDistance(offset.getImageryPos());
-        double direction = length < 1e-2 ? 0.0 : correctedCenterLL.heading(offset.getImageryPos());
-        // todo: north vs south. Meanwhile, let's fix this dirty:
-//        direction = Math.PI - direction;
-        if( direction < 0 )
-            direction += Math.PI * 2;
-        return new double[] {length, direction};
-    }
-
+    /**
+     * Converts distance in meters to a human-readable string.
+     */
     public static String formatDistance( double d ) {
-        if( d < 0.0095 ) return formatDistance(d * 1000, tr("mm"), false);
+        if( d < 0.0095 ) return formatDistance(d * 1000, tr("mm"), true);
         if( d < 0.095 )  return formatDistance(d * 100,  tr("cm"), true );
         if( d < 0.95 )   return formatDistance(d * 100,  tr("cm"), false);
@@ -196,10 +143,13 @@
     }
 
+    /**
+     * Constructs a distance string.
+     * @param d Distance.
+     * @param si Units of measure for distance.
+     * @param floating Whether a floating point is needed.
+     * @return A formatted string.
+     */
     private static String formatDistance( double d, String si, boolean floating ) {
         return MessageFormat.format(floating ? "{0,number,0.0} {1}" : "{0,number,0} {1}", d, si);
     }
-
-    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 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialog.java	(revision 29384)
@@ -5,4 +5,7 @@
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
 import java.util.*;
 import java.util.List;
@@ -17,12 +20,12 @@
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.layer.MapViewPaintable;
+import org.openstreetmap.josm.tools.*;
 import static org.openstreetmap.josm.tools.I18n.tr;
-import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.OpenBrowser;
 
 /**
  * The dialog which presents a choice between imagery align options.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class OffsetDialog extends JDialog implements ActionListener, NavigatableComponent.ZoomChangeListener, MapViewPaintable {
@@ -30,5 +33,11 @@
     protected static final String PREF_DEPRECATED = "iodb.show.deprecated";
     private static final int MAX_OFFSETS = Main.main.pref.getInteger("iodb.max.offsets", 5);
-    private static final boolean MODAL = false; // modal does not work for executing actions
+
+    /**
+     * Whether to create a modal frame. It turns out, modal dialogs
+     * block swing worker thread, so offset deprecation, for example, takes
+     * place only after the dialog is closed. Very inconvenient.
+     */
+    private static final boolean MODAL = false;
 
     private List<ImageryOffsetBase> offsets;
@@ -36,4 +45,8 @@
     private JPanel buttonPanel;
 
+    /**
+     * Initialize the dialog and install listeners. 
+     * @param offsets The list of offset to choose from.
+     */
     public OffsetDialog( List<ImageryOffsetBase> offsets ) {
         super(JOptionPane.getFrameForComponent(Main.parent), ImageryOffsetTools.DIALOG_TITLE,
@@ -50,4 +63,7 @@
     }
     
+    /**
+     * Creates the GUI.
+     */
     private void prepareDialog() {
         updateButtonPanel();
@@ -89,4 +105,8 @@
     }
 
+    /**
+     * As the name states, this method updates the button panel. It is called
+     * when a user clicks filtering checkboxes or deprecates an offset.
+     */
     private void updateButtonPanel() {
         List<ImageryOffsetBase> filteredOffsets = filterOffsets();
@@ -112,4 +132,8 @@
     }
 
+    /**
+     * Make a filtered offset list out of the full one. Takes into
+     * account both checkboxes.
+     */
     private List<ImageryOffsetBase> filterOffsets() {
         boolean showCalibration = Main.pref.getBoolean(PREF_CALIBRATION, true);
@@ -128,4 +152,8 @@
     }
 
+    /**
+     * This listener method is called when a user pans or zooms the map.
+     * It does nothing, only passes the event to all displayed offset buttons.
+     */
     public void zoomChanged() {
         for( Component c : buttonPanel.getComponents() ) {
@@ -136,4 +164,8 @@
     }
 
+    /**
+     * Draw dots on the map where offsets are located. I doubt it has practical
+     * value, but looks nice.
+     */
     public void paint( Graphics2D g, MapView mv, Bounds bbox ) {
         if( offsets == null )
@@ -152,4 +184,11 @@
     }
     
+    /**
+     * Display the dialog and get the return value is case of a modal frame.
+     * Creates GUI, install a temporary map layer (see {@link #paint} and
+     * shows the window.
+     * @return Null for a non-modal dialog, the selected offset
+     * (or, again, a null value) otherwise.
+     */
     public ImageryOffsetBase showDialog() {
         // todo: add a temporary layer showing all offsets
@@ -164,4 +203,32 @@
     }
 
+    /**
+     * This is a listener method for all buttons (except "Help").
+     * It assigns a selected offset value and closes the dialog.
+     * If the dialog wasn't modal, it applies the offset immediately.
+     * Should it apply the offset either way? Probably.
+     * @see #applyOffset()
+     */
+    public void actionPerformed( ActionEvent e ) {
+        if( e.getSource() instanceof OffsetDialogButton ) {
+            selectedOffset = ((OffsetDialogButton)e.getSource()).getOffset();
+        } else
+            selectedOffset = null;
+        NavigatableComponent.removeZoomChangeListener(this);
+        setVisible(false);
+        if( !MODAL ) {
+            Main.map.mapView.removeTemporaryLayer(this);
+            Main.map.mapView.repaint();
+            if( selectedOffset != null )
+                applyOffset();
+        }
+    }
+
+
+    /**
+     * Either applies imagery offset or adds a calibration geometry layer.
+     * If the offset for each type was chosen for the first time ever,
+     * it displays an informational message.
+     */
     public void applyOffset() {
         if( selectedOffset instanceof ImageryOffset ) {
@@ -191,26 +258,20 @@
     }
 
-    public void actionPerformed( ActionEvent e ) {
-        if( e.getSource() instanceof OffsetDialogButton ) {
-            selectedOffset = ((OffsetDialogButton)e.getSource()).getOffset();
-        } else
-            selectedOffset = null;
-        NavigatableComponent.removeZoomChangeListener(this);
-        setVisible(false);
-        if( !MODAL ) {
-            Main.map.mapView.removeTemporaryLayer(this);
-            Main.map.mapView.repaint();
-            if( selectedOffset != null )
-                applyOffset();
-        }
-    }
-
+    /**
+     * A lisntener for successful deprecations.
+     */
     private class DeprecateOffsetListener implements QuerySuccessListener {
         ImageryOffsetBase offset;
 
+        /**
+         * Initialize the listener with an offset.
+         */
         public DeprecateOffsetListener( ImageryOffsetBase offset ) {
             this.offset = offset;
         }
 
+        /**
+         * Remove the deprecated offset from the offsets list. Then rebuild the button panel.
+         */
         public void queryPassed() {
             offset.setDeprecated(new Date(), JosmUserIdentityManager.getInstance().getUserName(), "");
@@ -219,4 +280,7 @@
     }
 
+    /**
+     * Opens a web browser with the wiki page in user's language.
+     */
     class HelpAction extends AbstractAction {
 
@@ -227,7 +291,18 @@
 
         public void actionPerformed( ActionEvent e ) {
-            String base = "http://wiki.openstreetmap.org/wiki/";
+            String base = Main.pref.get("url.openstreetmap-wiki", "http://wiki.openstreetmap.org/wiki/");
+            String lang = LanguageInfo.getWikiLanguagePrefix();
             String page = "Imagery_Offset_Database";
-            String lang = "RU:"; // todo: determine it
+            try {
+                // this logic was snatched from {@link org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog.HelpAction}
+                HttpURLConnection conn = Utils.openHttpConnection(new URL(base + lang + page));
+                conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect", 10) * 1000);
+                if( conn.getResponseCode() != 200 ) {
+                    conn.disconnect();
+                    lang = "";
+                }
+            } catch( IOException ex ) {
+                lang = "";
+            }
             OpenBrowser.displayUrl(base + lang + page);
         }
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java	(revision 29384)
@@ -5,11 +5,17 @@
 import javax.swing.ImageIcon;
 import javax.swing.JButton;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.tools.ImageProvider;
 
 /**
- * A button which shows offset information.
+ * A button which shows offset information. Must be spectacular, since it's the only
+ * non-JOptionPane GUI in the plugin.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class OffsetDialogButton extends JButton {
@@ -20,4 +26,8 @@
     private double direction;
 
+    /**
+     * Initialize the button with an offset. Calculated all relevant values.
+     * @param offset An offset to display on the button.
+     */
     public OffsetDialogButton( ImageryOffsetBase offset ) {
         super();
@@ -49,4 +59,7 @@
     }
 
+    /**
+     * Returns the offset associated with this button.
+     */
     public ImageryOffsetBase getOffset() {
         return offset;
@@ -69,10 +82,37 @@
     }
 
+    /**
+     * Calculates length and direction for two points in the imagery offset object.
+     * @see #getLengthAndDirection(iodb.ImageryOffset, double, double) 
+     */
     private double[] getLengthAndDirection( ImageryOffset offset ) {
         ImageryLayer layer = ImageryOffsetTools.getTopImageryLayer();
         double[] dxy = layer == null ? new double[] {0.0, 0.0} : new double[] {layer.getDx(), layer.getDy()};
-        return ImageryOffsetTools.getLengthAndDirection((ImageryOffset)offset, dxy[0], dxy[1]);
+        return getLengthAndDirection(offset, dxy[0], dxy[1]);
     }
 
+    /**
+     * Calculates length and direction for two points in the imagery offset object
+     * taking into account an existing imagery layer offset.
+     *
+     * @see #getLengthAndDirection(iodb.ImageryOffset)
+     */
+    public static double[] getLengthAndDirection( ImageryOffset offset, double dx, double dy ) {
+        Projection proj = Main.getProjection();
+        EastNorth pos = proj.latlon2eastNorth(offset.getPosition());
+        LatLon correctedCenterLL = proj.eastNorth2latlon(pos.add(-dx, -dy));
+        double length = correctedCenterLL.greatCircleDistance(offset.getImageryPos());
+        double direction = length < 1e-2 ? 0.0 : correctedCenterLL.heading(offset.getImageryPos());
+        // todo: north vs south. Meanwhile, let's fix this dirty:
+//        direction = Math.PI - direction;
+        if( direction < 0 )
+            direction += Math.PI * 2;
+        return new double[] {length, direction};
+    }
+
+    /**
+     * An offset icon. Displays a plain calibration icon for a geometry
+     * and an arrow for an imagery offset.
+     */
     class OffsetIcon implements Icon {
         private boolean isDeprecated;
@@ -82,4 +122,8 @@
         private ImageIcon background;
 
+        /**
+         * Initialize the icon with an offset object. Calculates length and direction
+         * of an arrow if they're needed.
+         */
         public OffsetIcon( ImageryOffsetBase offset ) {
             isDeprecated = offset.isDeprecated();
@@ -89,5 +133,5 @@
                 ImageryLayer layer = ImageryOffsetTools.getTopImageryLayer();
                 double[] dxy = layer == null ? new double[] {0.0, 0.0} : new double[] { layer.getDx(), layer.getDy() };
-                double[] ld = ImageryOffsetTools.getLengthAndDirection((ImageryOffset)offset, dxy[0], dxy[1]);
+                double[] ld = getLengthAndDirection((ImageryOffset)offset, dxy[0], dxy[1]);
                 length = ld[0];
                 direction = ld[1];
@@ -97,4 +141,7 @@
         }
 
+        /**
+         * Paints the base image and adds to it according to the offset.
+         */
         public void paintIcon( Component comp, Graphics g, int x, int y ) {
             background.paintIcon(comp, g, x, y);
@@ -110,5 +157,5 @@
                 } else {
                     // draw an arrow
-                    double arrowLength = length < 5 ? getIconWidth() / 2 - 1 : getIconWidth() - 4;
+                    double arrowLength = length < 10 ? getIconWidth() / 2 - 1 : getIconWidth() - 4;
                     g2.setStroke(new BasicStroke(2));
                     int dx = (int)Math.round(Math.sin(direction) * arrowLength / 2);
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetInfoAction.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetInfoAction.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetInfoAction.java	(revision 29384)
@@ -10,13 +10,18 @@
 
 /**
- * Download a list of imagery offsets for the current position, let user choose which one to use.
+ * Display an information box for an offset.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class OffsetInfoAction extends AbstractAction {
-    public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+    public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd MMMM yyyy");
 
     private Object info;
     
+    /**
+     * Initializes the action with an offset object.
+     * Calls {@link #getInformationObject(iodb.ImageryOffsetBase)}.
+     */
     public OffsetInfoAction( ImageryOffsetBase offset ) {
         super(tr("Offset Information"));
@@ -27,8 +32,14 @@
     }
 
+    /**
+     * Shows a dialog with the pre-constructed message.
+     */
     public void actionPerformed(ActionEvent e) {
         JOptionPane.showMessageDialog(Main.parent, info, ImageryOffsetTools.DIALOG_TITLE, JOptionPane.PLAIN_MESSAGE);
     }
 
+    /**
+     * Constructs a string with all information about the given offset.
+     */
     public static Object getInformationObject( ImageryOffsetBase offset ) {
         StringBuilder sb = new StringBuilder();
@@ -61,4 +72,8 @@
     }
 
+    /**
+     * Explains a calibration object geometry type: whether is's a point,
+     * a path or a polygon.
+     */
     public static String getGeometryType( CalibrationObject obj ) {
         if( obj.getGeometry() == null )
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/QuerySuccessListener.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/QuerySuccessListener.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/QuerySuccessListener.java	(revision 29384)
@@ -4,5 +4,6 @@
  * A listener for {@link SimpleOffsetQueryTask}.
  *
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public interface QuerySuccessListener {
Index: applications/editors/josm/plugins/imagery_offset_db/src/iodb/SimpleOffsetQueryTask.java
===================================================================
--- applications/editors/josm/plugins/imagery_offset_db/src/iodb/SimpleOffsetQueryTask.java	(revision 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/SimpleOffsetQueryTask.java	(revision 29384)
@@ -9,11 +9,11 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
-import org.openstreetmap.josm.io.OsmTransferException;
-import org.xml.sax.SAXException;
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 /**
+ * A task to query the imagery offset server and process the response.
  *
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 class SimpleOffsetQueryTask extends PleaseWaitRunnable {
@@ -24,4 +24,9 @@
     private QuerySuccessListener listener;
 
+    /**
+     * Initialize the task.
+     * @param query A query string, usually starting with an action word and a question mark.
+     * @param title A title for the progress monitor.
+     */
     public SimpleOffsetQueryTask( String query, String title ) {
         super(tr("Uploading"));
@@ -31,18 +36,32 @@
     }
 
+    /**
+     * In case a query was not specified when the object was constructed,
+     * it can be set with this method.
+     * @see #SimpleOffsetQueryTask(java.lang.String, java.lang.String) 
+     */
     public void setQuery( String query ) {
         this.query = query;
     }
 
+    /**
+     * Install a listener for successful responses. There can be only one.
+     */
     public void setListener( QuerySuccessListener listener ) {
         this.listener = listener;
     }
 
+    /**
+     * Remove a listener for successful responses.
+     */
     public void removeListener() {
         this.listener = null;
     }
 
+    /**
+     * The main method: calls {@link #doQuery(java.lang.String)} and processes exceptions.
+     */
     @Override
-    protected void realRun() throws SAXException, IOException, OsmTransferException {
+    protected void realRun() {
         getProgressMonitor().indeterminateSubTask(title);
         try {
@@ -56,8 +75,16 @@
     }
 
+    /**
+     * Sends a request to the imagery offset server. Processes exceptions and
+     * return codes, calls {@link #processResponse(java.io.InputStream)} on success.
+     * @param query
+     * @throws iodb.SimpleOffsetQueryTask.UploadException
+     * @throws IOException 
+     */
     private void doQuery( String query ) throws UploadException, IOException {
         try {
-            URL url = new URL(ImageryOffsetTools.getServerURL() + query);
-            System.out.println("url=" + url); // todo: remove in release
+            String serverURL = Main.pref.get("iodb.server.url", "http://offsets.textual.ru/");
+            URL url = new URL(serverURL + query);
+            Main.info("IODB URL = " + url); // todo: remove in release
             HttpURLConnection connection = (HttpURLConnection)url.openConnection();
             connection.connect();
@@ -79,4 +106,7 @@
     }
 
+    /**
+     * Doesn't actually cancel, just raises a flag.
+     */
     @Override
     protected void cancel() {
@@ -84,4 +114,8 @@
     }
 
+    /**
+     * Is called after {@link #realRun()}. Either displays an error message
+     * or notifies a listener of success.
+     */
     @Override
     protected void finish() {
@@ -93,4 +127,9 @@
     }
 
+    /**
+     * Parse the response input stream and determine whether an operation
+     * was successful or not.
+     * @throws iodb.SimpleOffsetQueryTask.UploadException Thrown if an error message was found.
+     */
     protected void processResponse( InputStream inp ) throws UploadException {
         String response = "";
@@ -110,4 +149,7 @@
     }
 
+    /**
+     * A placeholder exception for error messages.
+     */
     public static class UploadException extends Exception {
         public UploadException( String 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 29382)
+++ applications/editors/josm/plugins/imagery_offset_db/src/iodb/StoreImageryOffsetAction.java	(revision 29384)
@@ -15,16 +15,28 @@
 
 /**
- * Upload the current imagery offset or an calibration object information.
+ * Upload the current imagery offset or an calibration geometry information.
  * 
- * @author zverik
+ * @author Zverik
+ * @license WTFPL
  */
 public class StoreImageryOffsetAction extends JosmAction {
 
+    /**
+     * Initializes the action.
+     */
     public StoreImageryOffsetAction() {
         super(tr("Store Imagery Offset..."), "storeoffset",
                 tr("Upload an offset for current imagery (or calibration object information) to a server"),
-                null, false);
+                null, true);
     }
 
+    /**
+     * Asks user for description and calls the upload task.
+     * Also calculates a lot of things, checks whether the selected object
+     * is suitable for calibration geometry, constructs a map of query parameters etc.
+     * The only thing it doesn't do is check for the real user account name.
+     * This is because all server queries should be executed in workers,
+     * and we don't have one when a user name is needed.
+     */
     public void actionPerformed(ActionEvent e) {
         if( Main.map == null || Main.map.mapView == null )
@@ -36,5 +48,5 @@
 
         String userName = JosmUserIdentityManager.getInstance().getUserName();
-        if( userName == null ) {
+        if( userName == null || userName.length() == 0 ) {
             JOptionPane.showMessageDialog(Main.parent, tr("To store imagery offsets you must be a registered OSM user."), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.ERROR_MESSAGE);
             return;
@@ -108,4 +120,10 @@
     }
 
+    /**
+     * Ask a user for a description / reason. This string should be 3 to 200 characters
+     * long, and the method enforces that.
+     * @param message A prompt for the input dialog.
+     * @return Either null or a string 3 to 200 letters long.
+     */
     public static String queryDescription( Object message ) {
         String reason = null;
@@ -129,4 +147,8 @@
     }
 
+    /**
+     * This action is enabled when there's a map and a visible imagery layer.
+     * Note that it doesn't require edit layer.
+     */
     @Override
     protected void updateEnabledState() {
