Index: /applications/editors/josm/plugins/imagery_offset_db/build.xml
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/build.xml	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/build.xml	(revision 27986)
@@ -0,0 +1,245 @@
+<?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_offset_db" default="dist" basedir=".">
+    <!-- enter the SVN commit message -->
+    <property name="commit.message" value="Imagery Offset Database"/>
+    <!-- enter the *lowest* JOSM version this plugin is currently compatible with -->
+    <property name="plugin.main.version" value="4549"/>
+    <!--
+      ************************************************
+      ** should not be necessary to change the following properties
+     -->
+    <property name="josm" location="../../core/dist/josm-custom.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="plugin.dist.dir"        value="/Users/Zverik/AppData/Roaming/JOSM/plugins"/-->
+    <property name="ant.build.javac.target" value="1.5"/>
+    <property name="ant.build.javac.source" value="1.5"/>
+    <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}" 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}/images">
+            <fileset dir="images"/>
+        </copy>
+        <copy todir="${plugin.build.dir}/data">
+            <fileset dir="data"/>
+        </copy>
+        <copy todir="${plugin.build.dir}">
+            <fileset dir="src" includes="**/*.txt"/>
+        </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="Ilya Zverev"/>
+                <attribute name="Plugin-Class" value="iodb.ImageryOffsetPlugin"/>
+                <attribute name="Plugin-Date" value="${version.entry.commit.date}"/>
+                <attribute name="Plugin-Description" value="Database of imagery offsets: share and aquire imagery offsets with one button."/>
+                <attribute name="ru_Plugin-Description" value="База данных смещений подложек: загружайте и делитесь смещениями одной кнопкой."/>
+                <attribute name="Plugin-Icon" value="images/iodb.png"/>
+		<attribute name="Plugin-Link" value="http://wiki.openstreetmap.org/wiki/JOSM/Plugins/ImageryOffsetDB"/>
+                <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>
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java	(revision 27986)
@@ -0,0 +1,30 @@
+package iodb;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.User;
+
+/**
+ *
+ * @author zverik
+ */
+public class CalibrationObject extends ImageryOffsetBase {
+    private OsmPrimitive object;
+    private long lastUserId;
+
+    public CalibrationObject(OsmPrimitive object, long lastUserId) {
+        this.object = object;
+        this.lastUserId = lastUserId;
+    }
+
+    public CalibrationObject(OsmPrimitive object) {
+        this(object, -1);
+    }
+
+    public long getLastUserId() {
+        return lastUserId;
+    }
+
+    public OsmPrimitive getObject() {
+        return object;
+    }
+}
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java	(revision 27986)
@@ -0,0 +1,194 @@
+package iodb;
+
+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 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.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;
+
+/**
+ * Download a list of imagery offsets for the current position, let user choose which one to use.
+ * 
+ * @author zverik
+ */
+public class GetImageryOffsetAction extends JosmAction {
+    
+    private List<ImageryOffsetBase> offsets;
+    
+    private HashMap<String, String> imageryAliases;
+    
+    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();
+    }
+
+    public void actionPerformed(ActionEvent e) {
+        Projection proj = Main.map.mapView.getProjection();
+        LatLon center = proj.eastNorth2latlon(Main.map.mapView.getCenter());
+        // todo: download a list of offsets for current bbox * N
+        List<ImageryOffsetBase> offsets = download(center); // todo: async
+        DownloadOffsets download = new DownloadOffsets();
+        Future<?> future = Main.worker.submit(download);
+        try {
+            future.get();
+        } catch( Exception ex ) {
+            ex.printStackTrace();
+            return;
+        }
+        
+        // todo: show a dialog for selecting one of the offsets (without "update" flag)
+        ImageryOffsetBase offset = new OffsetDialog(offsets).showDialog();
+        if( offset != null ) {
+            // todo: use the chosen offset
+        }
+    }
+    
+    private List<ImageryOffsetBase> download( LatLon center ) {
+        String base = Main.pref.get("iodb.server.url", "http://textual.ru/iodb.php");
+        String query = "?action=get&lat=" + center.getX() + "&lon=" + center.getY();
+        List<ImageryOffsetBase> result = null;
+        try {
+            query = query + "&imagery=" + URLEncoder.encode(getImageryID(), "utf-8");
+            URL url = new URL(base + query);
+            HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+            connection.connect();
+            int retCode = connection.getResponseCode();
+            InputStream inp = connection.getInputStream();
+            if( inp != null ) {
+                result = new IODBReader(inp).parse();
+            }
+            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;
+    }
+    
+    private String getImageryID() {
+        List<ImageryLayer> layers = Main.map.mapView.getLayersOfType(ImageryLayer.class);
+        String url = null;
+        for( ImageryLayer layer : layers ) {
+            if( layer.isVisible() ) {
+                url = layer.getInfo().getUrl();
+                break;
+            }
+        }
+        if( url == null )
+            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 void loadImageryAliases() {
+        if( imageryAliases == null )
+            imageryAliases = new HashMap<String, String>();
+        else
+            imageryAliases.clear();
+        
+        // { substring, alias }
+        imageryAliases.put("bing", "bing");
+        // todo: load from a resource?
+    }
+    
+    // Following three methods were snatched from TMSLayer
+    private double latToTileY(double lat, int zoom) {
+        double l = lat / 180 * Math.PI;
+        double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
+        return Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI;
+    }
+
+    private double lonToTileX(double lon, int zoom) {
+        return Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0;
+    }
+
+    private int getCurrentZoom() {
+        if (Main.map == null || Main.map.mapView == null) {
+            return 1;
+        }
+        MapView mv = Main.map.mapView;
+        LatLon topLeft = mv.getLatLon(0, 0);
+        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+        double x1 = lonToTileX(topLeft.lon(), 1);
+        double y1 = latToTileY(topLeft.lat(), 1);
+        double x2 = lonToTileX(botRight.lon(), 1);
+        double y2 = latToTileY(botRight.lat(), 1);
+
+        int screenPixels = mv.getWidth() * mv.getHeight();
+        double tilePixels = Math.abs((y2 - y1) * (x2 - x1) * 256 * 256);
+        if (screenPixels == 0 || tilePixels == 0) {
+            return 1;
+        }
+        double factor = screenPixels / tilePixels;
+        double result = Math.log(factor) / Math.log(2) / 2 + 1;
+        int intResult = (int) Math.floor(result);
+        return intResult;
+    }
+    
+    class DownloadOffsets extends PleaseWaitRunnable {
+        
+        private boolean cancelled;
+
+        public DownloadOffsets() {
+            super(tr("Downloading calibration data"));
+            cancelled = false;
+        }
+
+        @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
+        }
+        
+        @Override
+        protected void cancel() {
+            cancelled = true;
+        }
+    }
+}
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java	(revision 27986)
@@ -0,0 +1,192 @@
+package iodb;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.io.UTFInputStreamReader;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Parses the message from server. It expects XML in UTF-8 with several &lt;offset&gt; elements.
+ * 
+ * @author zverik
+ */
+public class IODBReader {
+    private List<ImageryOffsetBase> offsets;
+    private InputSource source;
+    
+    private class Parser extends DefaultHandler {
+        private StringBuffer accumulator = new StringBuffer();
+        private IOFields fields;
+        private boolean parsingOffset;
+        private SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd");
+
+        @Override
+        public void startDocument() throws SAXException {
+            fields = new IOFields();
+            offsets.clear();
+            parsingOffset = false;
+        }
+
+        private LatLon parseLatLon(Attributes atts) {
+            return new LatLon(
+                    Double.parseDouble(atts.getValue("lat")),
+                    Double.parseDouble(atts.getValue("lon")));
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+            if( !parsingOffset ) {
+                if( qName.equals("offset") || qName.equals("calibration-object") ) {
+                    parsingOffset = true;
+                    fields.clear();
+                    fields.position = parseLatLon(attributes);
+                }
+            } else {
+                if( qName.equals("object") ) {
+                    fields.isNode = attributes.getValue("type").equals("node");
+                } else if( qName.equals("imagery-position") ) {
+                    fields.imageryPos = parseLatLon(attributes);
+                } else if( qName.equals("imagery") ) {
+                    String minZoom = attributes.getValue("minzoom");
+                    String maxZoom = attributes.getValue("maxzoom");
+                    if( minZoom != null )
+                        fields.minZoom = Integer.parseInt(minZoom);
+                    if( maxZoom != null )
+                        fields.maxZoom = Integer.parseInt(maxZoom);
+                }
+            }
+            accumulator.setLength(0);
+        }
+        
+        @Override
+        public void characters(char[] ch, int start, int length) throws SAXException {
+            if( parsingOffset )
+                accumulator.append(ch, start, length);
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) throws SAXException {
+            if( parsingOffset ) {
+                if( qName.equals("author") ) {
+                    fields.author = accumulator.toString();
+                } else if( qName.equals("description") ) {
+                    fields.description = accumulator.toString();
+                } else if( qName.equals("date") ) {
+                    try {
+                        fields.date = dateParser.parse(accumulator.toString());
+                    } catch (ParseException ex) {
+                        throw new SAXException(ex);
+                    }
+                } else if( qName.equals("deprecated") ) {
+                    try {
+                        fields.abandonDate = dateParser.parse(accumulator.toString());
+                    } catch (ParseException ex) {
+                        throw new SAXException(ex);
+                    }
+                } else if( qName.equals("imagery") ) {
+                    fields.imagery = accumulator.toString();
+                } 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
+                    try {
+                        offsets.add(fields.constructObject());
+                    } catch( IllegalArgumentException ex ) {
+                        System.err.println(ex.getMessage());
+                    }
+                    parsingOffset = false;
+                }
+            }
+        }
+    }
+    
+
+    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);
+        }
+    }
+    
+    private class IOFields {
+        public LatLon position;
+        public Date date;
+        public String author;
+        public String description;
+        public Date abandonDate;
+        public LatLon imageryPos;
+        public String imagery;
+        public int minZoom, maxZoom;
+        public boolean isNode;
+        public long objectId;
+        public long lastUserId;
+
+        public IOFields() {
+            clear();
+        }
+        
+        public void clear() {
+            position = null;
+            date = null;
+            author = null;
+            description = null;
+            abandonDate = null;
+            imageryPos = null;
+            imagery = null;
+            minZoom = -1;
+            maxZoom = -1;
+            isNode = false;
+            objectId = -1;
+            lastUserId = -1;
+        }
+
+        public ImageryOffsetBase constructObject() {
+            if( author == null || description == null || position == null || date == null )
+                throw new IllegalArgumentException("Not enought arguments to build an object");
+            if( objectId < 0 ) {
+                if( imagery == null || imageryPos == null )
+                    throw new IllegalArgumentException("Both imagery and imageryPos should be sepcified for the offset");
+                ImageryOffset result = new ImageryOffset(imagery, imageryPos);
+                if( minZoom >= 0 )
+                    result.setMinZoom(minZoom);
+                if( maxZoom >= 0 )
+                    result.setMaxZoom(maxZoom);
+                result.setBasicInfo(position, author, description, date);
+                result.setAbandonDate(abandonDate);
+                return result;
+            } else {
+                OsmPrimitive p = isNode ? new Node(objectId) : new Way(objectId);
+                CalibrationObject result = new CalibrationObject(p, lastUserId);
+                result.setBasicInfo(position, author, description, date);
+                result.setAbandonDate(abandonDate);
+                return result;
+            }
+        }
+    }
+}
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffset.java	(revision 27986)
@@ -0,0 +1,45 @@
+package iodb;
+
+import org.openstreetmap.josm.data.coor.LatLon;
+
+/**
+ * An offset.
+ * 
+ * @author zverik
+ */
+public class ImageryOffset extends ImageryOffsetBase {
+    private LatLon imageryPos;
+    private String imagery;
+    private int minZoom, maxZoom;
+
+    public ImageryOffset( String imagery, LatLon imageryPos ) {
+        this.imageryPos = imageryPos;
+        this.imagery = imagery;
+        this.minZoom = 0;
+        this.maxZoom = 30;
+    }
+
+    public void setMaxZoom(int maxZoom) {
+        this.maxZoom = maxZoom;
+    }
+
+    public void setMinZoom(int minZoom) {
+        this.minZoom = minZoom;
+    }
+    
+    public LatLon getImageryPos() {
+        return imageryPos;
+    }
+
+    public String getImagery() {
+        return imagery;
+    }
+
+    public int getMaxZoom() {
+        return maxZoom;
+    }
+
+    public int getMinZoom() {
+        return minZoom;
+    }
+}
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java	(revision 27986)
@@ -0,0 +1,53 @@
+package iodb;
+
+import java.util.Date;
+import org.openstreetmap.josm.data.coor.LatLon;
+
+/**
+ * Stores one imagery offset record.
+ * 
+ * @author zverik
+ */
+public class ImageryOffsetBase {
+    private LatLon position;
+    private Date date;
+    private String author;
+    private String description;
+    private Date abandonDate;
+    
+    public void setBasicInfo( LatLon position, String author, String description, Date date ) {
+        this.position = position;
+        this.author = author;
+        this.description = description;
+        this.date = date;
+        this.abandonDate = null;
+    }
+
+    public void setAbandonDate(Date abandonDate) {
+        this.abandonDate = abandonDate;
+    }
+
+    public Date getAbandonDate() {
+        return abandonDate;
+    }
+    
+    public boolean isAbandoned() {
+        return abandonDate != null;
+    }
+
+    public String getAuthor() {
+        return author;
+    }
+
+    public Date getDate() {
+        return date;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public LatLon getPosition() {
+        return position;
+    }
+}
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetPlugin.java	(revision 27986)
@@ -0,0 +1,28 @@
+package iodb;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginInformation;
+
+/**
+ * Add some actions to the imagery menu.
+ * 
+ * @author zverik
+ */
+public class ImageryOffsetPlugin extends Plugin {
+    private GetImageryOffsetAction getAction;
+    private StoreImageryOffsetAction storeAction;
+    
+    public ImageryOffsetPlugin( PluginInformation info ) {
+        super(info);
+        
+        getAction = new GetImageryOffsetAction();
+        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
+    }
+}
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialog.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialog.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialog.java	(revision 27986)
@@ -0,0 +1,50 @@
+package iodb;
+
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+import javax.swing.*;
+import org.openstreetmap.josm.Main;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * The dialog which presents a choice between imagery align options.
+ * 
+ * @author zverik
+ */
+public class OffsetDialog extends JDialog {
+    private List<ImageryOffsetBase> offsets;
+    private int selectedOffset;
+
+    public OffsetDialog( List<ImageryOffsetBase> offsets ) {
+        super(JOptionPane.getFrameForComponent(Main.parent), tr("Imagery Offset"), ModalityType.DOCUMENT_MODAL);
+        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+        this.offsets = offsets;
+    }
+    
+    private void prepareDialog() {
+        JPanel buttonPanel = new JPanel(new GridLayout(offsets.size() + 1, 1));
+        for( ImageryOffsetBase offset : offsets ) {
+            buttonPanel.add(new OffsetDialogButton(offset));
+        }
+        JButton cancelButton = new JButton("Cancel");
+        cancelButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                selectedOffset = -1;
+                OffsetDialog.this.setVisible(false);
+            }
+        });
+        buttonPanel.add(cancelButton); // todo: proper button
+        setContentPane(buttonPanel);
+        pack();
+        setLocationRelativeTo(Main.parent);
+    }
+    
+    public ImageryOffsetBase showDialog() {
+        selectedOffset = -1;
+        prepareDialog();
+        setVisible(true);
+        return selectedOffset < 0 ? null : offsets.get(selectedOffset);
+    }
+}
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialogButton.java	(revision 27986)
@@ -0,0 +1,16 @@
+package iodb;
+
+import javax.swing.JButton;
+
+/**
+ * A button which shows offset information.
+ * 
+ * @author zverik
+ */
+public class OffsetDialogButton extends JButton {
+
+    public OffsetDialogButton( ImageryOffsetBase offset ) {
+        super(offset.getDescription() + " (" + offset.getPosition().lat() + ", " + offset.getPosition().lon() + ")");
+    }
+    
+}
Index: /applications/editors/josm/plugins/imagery_offset_db/src/iodb/StoreImageryOffsetAction.java
===================================================================
--- /applications/editors/josm/plugins/imagery_offset_db/src/iodb/StoreImageryOffsetAction.java	(revision 27986)
+++ /applications/editors/josm/plugins/imagery_offset_db/src/iodb/StoreImageryOffsetAction.java	(revision 27986)
@@ -0,0 +1,29 @@
+package iodb;
+
+import java.awt.event.ActionEvent;
+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 static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * Upload the current imagery offset or an calibration object information.
+ * 
+ * @author zverik
+ */
+public class StoreImageryOffsetAction extends JosmAction {
+
+    public StoreImageryOffsetAction() {
+        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) {
+        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
+    }
+}
