Index: /applications/editors/josm/plugins/NanoLog/build.xml
===================================================================
--- /applications/editors/josm/plugins/NanoLog/build.xml	(revision 30490)
+++ /applications/editors/josm/plugins/NanoLog/build.xml	(revision 30491)
@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <project name="NanoLog" default="dist" basedir=".">
+
     <!-- enter the SVN commit message -->
     <property name="commit.message" value="NanoLog"/>
@@ -23,196 +24,21 @@
     **********************************************************
     -->
-    <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="nanolog.NanoLogPlugin"/>
-                <attribute name="Plugin-Date" value="${version.entry.commit.date}"/>
-                <attribute name="Plugin-Description" value="NanoLog adjustment and browsing layer"/>
-                <attribute name="ru_Plugin-Description" value="Загрузчик слоя NanoLog"/>
-                <attribute name="Plugin-Icon" value="images/nanolog.png"/>
-		<attribute name="Plugin-Link" value="http://wiki.openstreetmap.org/wiki/NanoLog"/>
-		<attribute name="ru_Plugin-Link" value="http://wiki.openstreetmap.org/wiki/RU:NanoLog"/>
-                <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>
+    <property name="plugin.author" value="Ilya Zverev"/>
+    <property name="plugin.class" value="nanolog.NanoLogPlugin"/>
+    <property name="plugin.description" value="NanoLog adjustment and btowsing layer"/>
+    <property name="plugin.icon" value="images/nanolog.png"/>
+    <!--<property name="plugin.early" value="..."/>-->
+    <!--<property name="plugin.requires" value="..."/>-->
+    <!--<property name="plugin.stage" value="..."/>-->
+	<property name="plugin.description.ru" value="Загрузчик слоя NanoLog"/>
+	<property name="plugin.link" value="http://wiki.openstreetmap.org/wiki/NanoLog"/>
+	<property name="plugin.link.ru" value="http://wiki.openstreetmap.org/wiki/RU:NanoLog"/>
+
+    
+	<property name="josm" location="../../core/dist/josm-custom.jar"/>
+	<property name="plugin.dist.dir" value="../../dist"/>
+	
+    <!-- ** include targets that all plugins have in common ** -->
+    <import file="../build-common.xml"/>
+  
 </project>
Index: /applications/editors/josm/plugins/NanoLog/src/nanolog/Correlator.java
===================================================================
--- /applications/editors/josm/plugins/NanoLog/src/nanolog/Correlator.java	(revision 30491)
+++ /applications/editors/josm/plugins/NanoLog/src/nanolog/Correlator.java	(revision 30491)
@@ -0,0 +1,259 @@
+package nanolog;
+
+import java.text.ParseException;
+import java.util.*;
+import javax.swing.JOptionPane;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.gpx.*;
+import org.openstreetmap.josm.tools.Geometry;
+import static org.openstreetmap.josm.tools.I18n.tr;
+import org.openstreetmap.josm.tools.PrimaryDateParser;
+
+/**
+ * A class that establishes correlation between GPS trace and NanoLog. Mostly copied from
+ * {@link org.openstreetmap.josm.gui.layer.geoimage.CorrelateGpxWithImages}, thus licensed GPL.
+ *
+ * @author zverik
+ */
+public class Correlator {
+
+    /**
+     * Matches entries to GPX so most points are on the trace.
+     */
+    public static long crudeMatch( List<NanoLogEntry> entries, GpxData data ) {
+        List<NanoLogEntry> sortedEntries = new ArrayList<NanoLogEntry>(entries);
+        PrimaryDateParser dateParser = new PrimaryDateParser();
+        Collections.sort(sortedEntries);
+        long firstExifDate = sortedEntries.get(0).getTime().getTime();
+        long firstGPXDate = -1;
+        outer:
+        for( GpxTrack trk : data.tracks ) {
+            for( GpxTrackSegment segment : trk.getSegments() ) {
+                for( WayPoint curWp : segment.getWayPoints() ) {
+                    String curDateWpStr = (String)curWp.attr.get("time");
+                    if( curDateWpStr == null ) {
+                        continue;
+                    }
+
+                    try {
+                        firstGPXDate = dateParser.parse(curDateWpStr).getTime();
+                        break outer;
+                    } catch( Exception e ) {
+                        Main.warn(e);
+                    }
+                }
+            }
+        }
+
+        // No GPX timestamps found, exit
+        if( firstGPXDate < 0 ) {
+            JOptionPane.showMessageDialog(Main.parent,
+                    tr("The selected GPX track does not contain timestamps. Please select another one."),
+                    tr("GPX Track has no time information"), JOptionPane.WARNING_MESSAGE);
+            return 0;
+        }
+
+        return firstExifDate - firstGPXDate;
+    }
+
+    public static void revertPos( List<NanoLogEntry> entries ) {
+        for( NanoLogEntry entry : entries ) {
+            entry.setPos(entry.getBasePos());
+        }
+    }
+
+    /**
+     * Offset is in 1/1000 of a second.
+     * @param entries
+     * @param data
+     * @param offset
+     */
+    public static void correlate( List<NanoLogEntry> entries, GpxData data, long offset ) {
+        List<NanoLogEntry> sortedEntries = new ArrayList<NanoLogEntry>(entries);
+        int ret = 0;
+        PrimaryDateParser dateParser = new PrimaryDateParser();
+        Collections.sort(sortedEntries);
+        for( GpxTrack track : data.tracks ) {
+            for( GpxTrackSegment segment : track.getSegments() ) {
+                long prevWpTime = 0;
+                WayPoint prevWp = null;
+
+                for( WayPoint curWp : segment.getWayPoints() ) {
+
+                    String curWpTimeStr = (String)curWp.attr.get("time");
+                    if( curWpTimeStr != null ) {
+                        try {
+                            long curWpTime = dateParser.parse(curWpTimeStr).getTime() + offset;
+                            ret += matchPoints(sortedEntries, prevWp, prevWpTime, curWp, curWpTime, offset);
+
+                            prevWp = curWp;
+                            prevWpTime = curWpTime;
+
+                        } catch( ParseException e ) {
+                            Main.error("Error while parsing date \"" + curWpTimeStr + '"');
+                            Main.error(e);
+                            prevWp = null;
+                            prevWpTime = 0;
+                        }
+                    } else {
+                        prevWp = null;
+                        prevWpTime = 0;
+                    }
+                }
+            }
+        }
+    }
+
+    private static int matchPoints( List<NanoLogEntry> entries, WayPoint prevWp, long prevWpTime,
+            WayPoint curWp, long curWpTime, long offset ) {
+        // Time between the track point and the previous one, 5 sec if first point, i.e. photos take
+        // 5 sec before the first track point can be assumed to be take at the starting position
+        long interval = prevWpTime > 0 ? Math.abs(curWpTime - prevWpTime) : 5 * 1000;
+        int ret = 0;
+
+        // i is the index of the timewise last photo that has the same or earlier EXIF time
+        int i = getLastIndexOfListBefore(entries, curWpTime);
+
+        // no photos match
+        if( i < 0 )
+            return 0;
+
+        Integer direction = null;
+        if( prevWp != null ) {
+            direction = Long.valueOf(Math.round(180.0 / Math.PI * prevWp.getCoor().heading(curWp.getCoor()))).intValue();
+        }
+
+        // First trackpoint, then interval is set to five seconds, i.e. photos up to five seconds
+        // before the first point will be geotagged with the starting point
+        if( prevWpTime == 0 || curWpTime <= prevWpTime ) {
+            while( true ) {
+                if( i < 0 ) {
+                    break;
+                }
+                final NanoLogEntry curImg = entries.get(i);
+                long time = curImg.getTime().getTime();
+                if( time > curWpTime || time < curWpTime - interval ) {
+                    break;
+                }
+                if( curImg.getPos() == null ) {
+                    curImg.setPos(curWp.getCoor());
+                    curImg.setDirection(direction);
+                    ret++;
+                }
+                i--;
+            }
+            return ret;
+        }
+
+        // This code gives a simple linear interpolation of the coordinates between current and
+        // previous track point assuming a constant speed in between
+        while( true ) {
+            if( i < 0 ) {
+                break;
+            }
+            NanoLogEntry curImg = entries.get(i);
+            long imgTime = curImg.getTime().getTime();
+            if( imgTime < prevWpTime ) {
+                break;
+            }
+
+            if( curImg.getPos() == null && prevWp != null ) {
+                // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless variable
+                double timeDiff = (double)(imgTime - prevWpTime) / interval;
+                curImg.setPos(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff));
+                curImg.setDirection(direction);
+
+                ret++;
+            }
+            i--;
+        }
+        return ret;
+    }
+
+    private static int getLastIndexOfListBefore(List<NanoLogEntry> entries, long searchedTime) {
+        int lstSize= entries.size();
+
+        // No photos or the first photo taken is later than the search period
+        if(lstSize == 0 || searchedTime < entries.get(0).getTime().getTime())
+            return -1;
+
+        // The search period is later than the last photo
+        if (searchedTime > entries.get(lstSize - 1).getTime().getTime())
+            return lstSize-1;
+
+        // The searched index is somewhere in the middle, do a binary search from the beginning
+        int curIndex= 0;
+        int startIndex= 0;
+        int endIndex= lstSize-1;
+        while (endIndex - startIndex > 1) {
+            curIndex= (endIndex + startIndex) / 2;
+            if (searchedTime > entries.get(curIndex).getTime().getTime()) {
+                startIndex= curIndex;
+            } else {
+                endIndex= curIndex;
+            }
+        }
+        if (searchedTime < entries.get(endIndex).getTime().getTime())
+            return startIndex;
+
+        // This final loop is to check if photos with the exact same EXIF time follows
+        while ((endIndex < (lstSize-1)) && (entries.get(endIndex).getTime().getTime()
+                == entries.get(endIndex + 1).getTime().getTime())) {
+            endIndex++;
+        }
+        return endIndex;
+    }
+
+    /**
+     * Returns date of a potential point on GPX track (which can be between points).
+     */
+    public static long getGpxDate( GpxData data, LatLon pos ) {
+        EastNorth en = Main.getProjection().latlon2eastNorth(pos);
+        PrimaryDateParser dateParser = new PrimaryDateParser();
+        for( GpxTrack track : data.tracks ) {
+            for( GpxTrackSegment segment : track.getSegments() ) {
+                long prevWpTime = 0;
+                WayPoint prevWp = null;
+                for( WayPoint curWp : segment.getWayPoints() ) {
+                    String curWpTimeStr = (String)curWp.attr.get("time");
+                    if( curWpTimeStr != null ) {
+                        try {
+                            long curWpTime = dateParser.parse(curWpTimeStr).getTime();
+                            if( prevWp != null ) {
+                                EastNorth c1 = Main.getProjection().latlon2eastNorth(prevWp.getCoor());
+                                EastNorth c2 = Main.getProjection().latlon2eastNorth(curWp.getCoor());
+                                if( !c1.equals(c2) ) {
+                                    EastNorth middle = Geometry.getSegmentAltituteIntersection(c1, c2, en);
+                                    if( middle != null && en.distance(middle) < 1 ) {
+                                        // found our point, no further search is neccessary
+                                        double prop = c1.east() == c2.east()
+                                                ? (middle.north() - c1.north()) / (c2.north() - c1.north())
+                                                : (middle.east() - c1.east()) / (c2.east() - c1.east());
+                                        if( prop >= 0 && prop <= 1 ) {
+                                            return Math.round(prevWpTime + prop * (curWpTime - prevWpTime));
+                                        }
+                                    }
+                                }
+                            }
+
+                            prevWp = curWp;
+                            prevWpTime = curWpTime;
+                        } catch( ParseException e ) {
+                            Main.error("Error while parsing date \"" + curWpTimeStr + '"');
+                            Main.error(e);
+                            prevWp = null;
+                            prevWpTime = 0;
+                        }
+                    } else {
+                        prevWp = null;
+                        prevWpTime = 0;
+                    }
+                }
+            }
+        }
+        return 0;
+    }
+}
Index: /applications/editors/josm/plugins/NanoLog/src/nanolog/GPXChooser.java
===================================================================
--- /applications/editors/josm/plugins/NanoLog/src/nanolog/GPXChooser.java	(revision 30491)
+++ /applications/editors/josm/plugins/NanoLog/src/nanolog/GPXChooser.java	(revision 30491)
@@ -0,0 +1,27 @@
+package nanolog;
+
+import javax.swing.*;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+
+/**
+ * A dialog to choose GPS trace.
+ *
+ * @author zverik
+ */
+public class GPXChooser extends JDialog {
+    public static GpxLayer chooseLayer() {
+        // temporary plug: return first found local layer
+        return topLayer();
+    }
+
+    public static GpxLayer topLayer() {
+        // return first found local layer
+        for( Layer layer : Main.map.mapView.getAllLayers() ) {
+            if( layer instanceof GpxLayer && ((GpxLayer)layer).isLocalFile() )
+                return (GpxLayer)layer;
+        }
+        return null;
+    }
+}
Index: /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogEntry.java
===================================================================
--- /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogEntry.java	(revision 30490)
+++ /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogEntry.java	(revision 30491)
@@ -1,6 +1,10 @@
 package nanolog;
 
-import java.util.Date;
+import java.awt.*;
+import java.util.*;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.gui.MapView;
 
 /**
@@ -9,12 +13,27 @@
  * @author zverik
  */
-public class NanoLogEntry {
+public class NanoLogEntry implements Comparable<NanoLogEntry> {
     private LatLon pos;
     private Date time;
     private String message;
-    private int direction;
-    private LatLon tmpPos;
+    private Integer direction;
+    private Integer baseDir;
+    private LatLon basePos;
 
-    public int getDirection() {
+    public NanoLogEntry( Date time, String message, LatLon basePos, Integer baseDir ) {
+        this.basePos = basePos;
+        this.baseDir = baseDir;
+        this.pos = basePos;
+        this.direction = baseDir;
+        this.time = time;
+        this.message = message;
+        this.direction = direction;
+    }
+
+    public NanoLogEntry( Date time, String message ) {
+        this(time, message, null, null);
+    }
+    
+    public Integer getDirection() {
         return direction;
     }
@@ -25,5 +44,21 @@
 
     public LatLon getPos() {
-        return tmpPos == null ? pos : tmpPos;
+        return pos;
+    }
+
+    public void setPos( LatLon pos ) {
+        this.pos = pos;
+    }
+
+    public void setDirection( Integer direction ) {
+        this.direction = direction;
+    }
+
+    public LatLon getBasePos() {
+        return basePos;
+    }
+
+    public Integer getBaseDir() {
+        return baseDir;
     }
 
@@ -31,3 +66,8 @@
         return time;
     }
+
+    @Override
+    public int compareTo( NanoLogEntry t ) {
+        return time.compareTo(t.time);
+    }
 }
Index: /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogLayer.java
===================================================================
--- /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogLayer.java	(revision 30490)
+++ /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogLayer.java	(revision 30491)
@@ -1,31 +1,28 @@
 package nanolog;
 
-import java.awt.Color;
-import java.awt.Graphics2D;
-import java.awt.Point;
-import java.awt.Rectangle;
-import java.awt.event.ActionEvent;
-import java.beans.PropertyChangeListener;
+import java.awt.*;
+import java.awt.event.*;
 import java.io.*;
-import java.util.ArrayList;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
 import java.util.List;
-import javax.swing.AbstractAction;
-import javax.swing.Action;
-import javax.swing.Icon;
+import java.util.regex.*;
+import javax.swing.*;
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.RenameLayerAction;
-import org.openstreetmap.josm.actions.SaveAction;
+import org.openstreetmap.josm.actions.*;
 import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.gpx.WayPoint;
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
 import org.openstreetmap.josm.gui.MapView;
-import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
-import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
-import org.openstreetmap.josm.gui.layer.JumpToMarkerActions;
-import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.dialogs.*;
+import org.openstreetmap.josm.gui.layer.*;
 import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trn;
 import org.openstreetmap.josm.tools.ImageProvider;
 
 /**
+ * NanoLog layer: a set of points that can be georeferenced.
  *
  * @author zverik
@@ -35,12 +32,52 @@
     private List<NanoLogEntry> log;
     private int selectedEntry;
+    private final Set<NanoLogLayerListener> listeners = new HashSet<NanoLogLayerListener>();
+    private NLLMouseAdapter mouseListener;
+    
+    public NanoLogLayer( List<NanoLogEntry> entries ) {
+        super(tr("NanoLog"));
+        log = new ArrayList<NanoLogEntry>(entries);
+        selectedEntry = -1;
+        mouseListener = new NLLMouseAdapter();
+        Main.map.mapView.addMouseListener(mouseListener);
+        Main.map.mapView.addMouseMotionListener(mouseListener);
+    }
+
+    @Override
+    public void destroy() {
+        Main.map.mapView.removeMouseListener(mouseListener);
+        Main.map.mapView.removeMouseMotionListener(mouseListener);
+        super.destroy();
+    }
     
     public NanoLogLayer( File file ) throws IOException {
-        super(tr("NanoLog"));
-        log = readNanoLog(file);
-        selectedEntry = -1;
+        this(readNanoLog(file));
+    }
+
+    public void addListener( NanoLogLayerListener listener ) {
+        listeners.add(listener);
+    }
+
+    public void removeListener( NanoLogLayerListener listener ) {
+        listeners.remove(listener);
+    }
+
+    protected void fireMarkersChanged() {
+        for( NanoLogLayerListener listener : listeners )
+            listener.markersUpdated(this);
+    }
+
+    protected void fireMarkerSelected() {
+        for( NanoLogLayerListener listener : listeners )
+            listener.markerActivated(this, selectedEntry < 0 ? null : log.get(selectedEntry));
+    }
+
+    public List<NanoLogEntry> getEntries() {
+        return Collections.unmodifiableList(log);
     }
     
     public static List<NanoLogEntry> readNanoLog( File file ) throws IOException {
+        final Pattern NANOLOG_LINE = Pattern.compile("(.+?)\\t(.+?)(?:\\s*\\{\\{(-?\\d+\\.\\d+),\\s*(-?\\d+\\.\\d+)(?:,\\s*(\\d+))?\\}\\})?");
+        final SimpleDateFormat fmt = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss.SS");
         List<NanoLogEntry> result = new ArrayList<NanoLogEntry>();
         BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF8"));
@@ -48,5 +85,31 @@
             String line = r.readLine();
             if( line != null ) {
-                // parse it
+                Matcher m = NANOLOG_LINE.matcher(line);
+                if( m.matches() ) {
+                    String time = m.group(1);
+                    String message = m.group(2);
+                    String lat = m.group(3);
+                    String lon = m.group(4);
+                    String dir = m.group(5);
+                    Date timeDate = null;
+                    try {
+                        timeDate = fmt.parse(time);
+                    } catch( ParseException e ) {
+                    }
+                    if( message == null || message.length() == 0 || timeDate == null )
+                        continue;
+                    LatLon pos = null;
+                    Integer direction = null;
+                    if( lat != null && lon != null ) {
+                        try {
+                            pos = new LatLon(Double.parseDouble(lat), Double.parseDouble(lon));
+                            direction = new Integer(dir);
+                        } catch( NumberFormatException e ) {
+                            // well...
+                        }
+                    }
+                    NanoLogEntry entry = new NanoLogEntry(timeDate, message, pos, direction);
+                    result.add(entry);
+                }
             }
         }
@@ -58,19 +121,12 @@
     public void paint( Graphics2D g, MapView mv, Bounds box ) {
         // todo
-        int radius = 4;
-        int width = Main.map.mapView.getWidth();
-        int height = Main.map.mapView.getHeight();
-        Rectangle clip = g.getClipBounds();
-        for( NanoLogEntry entry : log ) {
+        for( int i = 0; i < log.size(); i++  ) {
+            NanoLogEntry entry = log.get(i);
+            int radius = 4;
             if( entry.getPos() != null ) {
                 Point p = mv.getPoint(entry.getPos());
-                g.setColor(Color.green);
+                g.setColor(selectedEntry == i ? Color.red : Color.yellow);
                 g.fillOval(p.x - radius, p.y - radius, radius * 2, radius * 2);
             }
-        }
-        if( selectedEntry >= 0 && selectedEntry < log.size() ) {
-            Point p = mv.getPoint(log.get(selectedEntry).getPos());
-            g.setColor(Color.red);
-            g.fillOval(p.x - radius, p.y - radius, radius * 2, radius * 2);
         }
     }
@@ -120,5 +176,6 @@
             new RenameLayerAction(null, this),
             SeparatorLayerAction.INSTANCE,
-            new CorrelateEntries(),
+            new CorrelateEntries(true),
+            new CorrelateEntries(false),
             new SaveLayer(),
             SeparatorLayerAction.INSTANCE,
@@ -127,4 +184,5 @@
     }
 
+    @Override
     public void jumpToNextMarker() {
         selectedEntry++;
@@ -136,4 +194,5 @@
     }
 
+    @Override
     public void jumpToPreviousMarker() {
         selectedEntry--;
@@ -144,17 +203,150 @@
         Main.map.repaint();
     }
-    
-    private class CorrelateEntries extends AbstractAction {
-
+
+    protected void setSelected( int i ) {
+        int newSelected = i >= 0 && i < log.size() ? i : -1;
+        if( newSelected != selectedEntry ) {
+//            System.out.println("selected: " + log.get(newSelected).getMessage());
+            selectedEntry = newSelected;
+            fireMarkerSelected();
+            Main.map.mapView.repaint();
+        }
+    }
+
+    public void setSelected( NanoLogEntry entry ) {
+        if( entry == null )
+            setSelected(-1);
+        else {
+            for( int i = 0; i < log.size(); i++ ) {
+                if( entry.equals(log.get(i)) ) {
+                    setSelected(i);
+                    break;
+                }
+            }
+        }
+    }
+
+    private class NLLMouseAdapter extends MouseAdapter {
+        private int dragging;
+
+        public int nearestEntry( MouseEvent e ) {
+            LatLon ll = Main.map.mapView.getLatLon(e.getX(), e.getY());
+            int radius = 8;
+            if( ll != null ) {
+                LatLon lld = Main.map.mapView.getLatLon(e.getX() + radius, e.getY() + radius);
+                double distance = Math.max(lld.lat() - ll.lat(), lld.lon() - ll.lon());
+                boolean selectedIsSelected = false;
+                int newSelected = -1;
+                for( int i = 0; i < log.size(); i++ ) {
+                    if( log.get(i).getPos() != null && log.get(i).getPos().distance(ll) < distance ) {
+                        newSelected = i;
+                        if( i == selectedEntry )
+                            selectedIsSelected = true;
+                    }
+                }
+                if( newSelected >= 0 )
+                    return selectedIsSelected ? selectedEntry : newSelected;
+            }
+            return -1;
+        }
+
+        @Override
+        public void mouseMoved( MouseEvent e ) {
+            int nearest = nearestEntry(e);
+            if( nearest > 0 )
+                setSelected(nearest);
+        }
+
+        @Override
+        public void mouseDragged( MouseEvent e ) {
+            doDrag(e);
+        }
+
+        @Override
+        public void mouseReleased( MouseEvent e ) {
+            if( dragging > 0 ) {
+                dragging = 0;
+            }
+        }
+
+        @Override
+        public void mousePressed( MouseEvent e ) {
+            int nearest = nearestEntry(e);
+            if( nearest > 0 && Main.map.mapView.getActiveLayer() == NanoLogLayer.this ) {
+                dragging = nearest;
+                doDrag(e);
+            }
+        }
+
+        private void doDrag( MouseEvent e ) {
+            if( dragging > 0 )
+                dragTo(dragging, e.getX(), e.getY());
+        }
+    }
+
+    protected void dragTo( int entry, int x, int y ) {
+        GpxLayer gpx = GPXChooser.topLayer();
+        if( gpx == null )
+            return;
+        EastNorth eastNorth = Main.map.mapView.getEastNorth(x, y);
+        double tolerance = eastNorth.distance(Main.map.mapView.getEastNorth(x + 300, y));
+        WayPoint wp = gpx.data.nearestPointOnTrack(eastNorth, tolerance);
+        if( wp == null )
+            return;
+        long newTime = Correlator.getGpxDate(gpx.data, wp.getCoor());
+        if( newTime <= 0 )
+            return;
+        Correlator.revertPos(log);
+        Correlator.correlate(log, gpx.data, log.get(entry).getTime().getTime() - newTime);
+        Main.map.mapView.repaint();
+    }
+    
+    private class CorrelateEntries extends JosmAction {
+        private boolean toZero;
+
+        public CorrelateEntries() {
+            this(false);
+        }
+
+        public CorrelateEntries( boolean toZero ) {
+            super(toZero ? tr("Correlate with GPX...") : tr("Put on GPX..."), "nanolog/correlate", tr("Correlate entries with GPS trace"), null, false);
+            this.toZero = toZero;
+        }
+
+        @Override
+        public void actionPerformed( ActionEvent e ) {
+            // 1. Select GPX trace or display message to load one
+            // (better yet, disable when no GPX traces)
+            GpxLayer layer = GPXChooser.chooseLayer();
+            // 2. Correlate by default, sticking by date
+            // (if does not match, shift so hours-minutes stay)
+            if( layer != null ) {
+                long offset = toZero ? 0 : Correlator.crudeMatch(log, layer.data);
+                Correlator.revertPos(log);
+                Correlator.correlate(log, layer.data, offset);
+                fireMarkersChanged();
+                Main.map.mapView.repaint();
+            }
+            // 3. Show non-modal (?) window with a slider and a text input
+            // (todo: better slider, like in blender)
+        }
+    }
+    
+    private class SaveLayer extends JosmAction {
+
+        public SaveLayer() {
+            super(tr("Save layer..."), "nanolog/save", tr("Save NanoLog layer"), null, false);
+        }
+
+        @Override
         public void actionPerformed( ActionEvent e ) {
             // todo
-        }
-    }
-    
-    private class SaveLayer extends AbstractAction {
-
-        public void actionPerformed( ActionEvent e ) {
-            // todo
-        }
+            JOptionPane.showMessageDialog(Main.parent, "Sorry, no saving yet", "NanoLog", JOptionPane.ERROR_MESSAGE);
+        }
+    }
+
+    public static interface NanoLogLayerListener {
+        void markersUpdated( NanoLogLayer layer );
+        void markerActivated( NanoLogLayer layer, NanoLogEntry entry );
     }
 }
Index: /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogPanel.java
===================================================================
--- /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogPanel.java	(revision 30490)
+++ /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogPanel.java	(revision 30491)
@@ -1,6 +1,13 @@
 package nanolog;
 
+import java.awt.Rectangle;
+import java.text.SimpleDateFormat;
+import java.util.*;
 import javax.swing.*;
+import nanolog.NanoLogLayer.NanoLogLayerListener;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
+import org.openstreetmap.josm.gui.layer.Layer;
 import static org.openstreetmap.josm.tools.I18n.tr;
 import org.openstreetmap.josm.tools.Shortcut;
@@ -11,24 +18,75 @@
  * @author zverik
  */
-public class NanoLogPanel extends ToggleDialog {
-    private JList logPanel;
+public class NanoLogPanel extends ToggleDialog implements LayerChangeListener, NanoLogLayerListener {
+    private JList<String> logPanel;
     private LogListModel listModel;
     
     public NanoLogPanel() {
-        super(tr("NanoLog"), "nanolog", tr("Open NanoLog panel"), null, 150, true);
+        super(tr("NanoLog"), "nanolog", tr("Open NanoLog panel"), null, 150, false);
         
         listModel = new LogListModel();
-        logPanel = new JList(listModel);
+        logPanel = new JList<String>(listModel);
         createLayout(logPanel, true, null);
     }
+
+    public void updateMarkers() {
+        List<NanoLogEntry> entries = new ArrayList<NanoLogEntry>();
+        for( NanoLogLayer l : Main.map.mapView.getLayersOfType(NanoLogLayer.class) ) {
+            entries.addAll(l.getEntries());
+        }
+        listModel.setEntries(entries);
+    }
+
+    @Override
+    public void activeLayerChange( Layer oldLayer, Layer newLayer ) {
+        // todo
+    }
+
+    @Override
+    public void layerAdded( Layer newLayer ) {
+        if( newLayer instanceof NanoLogLayer )
+            ((NanoLogLayer)newLayer).addListener(this);
+        updateMarkers();
+    }
+
+    @Override
+    public void layerRemoved( Layer oldLayer ) {
+        updateMarkers();
+    }
+
+    @Override
+    public void markersUpdated( NanoLogLayer layer ) {
+        updateMarkers();
+    }
+
+    @Override
+    public void markerActivated( NanoLogLayer layer, NanoLogEntry entry ) {
+        int idx = entry == null ? -1 : listModel.find(entry);
+        if( idx >= 0 ) {
+            logPanel.setSelectedIndex(idx);
+            Rectangle rect = logPanel.getCellBounds(Math.max(0, idx-2), Math.min(idx+4, listModel.getSize()));
+            logPanel.scrollRectToVisible(rect);
+        }
+    }
     
-    private class LogListModel extends AbstractListModel {
+    private class LogListModel extends AbstractListModel<String> {
+        private List<NanoLogEntry> entries;
+        private final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss");
 
         public int getSize() {
-            return 0;
+            return entries.size();
         }
 
         public String getElementAt( int index ) {
-            return ""; // todo
+            return TIME_FORMAT.format(entries.get(index).getTime()) + " " + entries.get(index).getMessage();
+        }
+
+        public void setEntries( List<NanoLogEntry> entries ) {
+            this.entries = entries;
+            fireContentsChanged(this, 0, entries.size());
+        }
+
+        public int find( NanoLogEntry entry ) {
+            return entries.indexOf(entry);
         }
     }
Index: /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogPlugin.java
===================================================================
--- /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogPlugin.java	(revision 30490)
+++ /applications/editors/josm/plugins/NanoLog/src/nanolog/NanoLogPlugin.java	(revision 30491)
@@ -2,8 +2,12 @@
 
 import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.util.List;
 import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.plugins.Plugin;
 import org.openstreetmap.josm.plugins.PluginInformation;
@@ -18,5 +22,5 @@
     public NanoLogPlugin( PluginInformation info ) {
         super(info);
-        Main.main.menu.fileMenu.add(new OpenNanoLogLayerAction());
+        Main.main.menu.fileMenu.insert(new OpenNanoLogLayerAction(), 4);
     }
     
@@ -24,5 +28,7 @@
     public void mapFrameInitialized( MapFrame oldFrame, MapFrame newFrame ) {
         if( oldFrame == null && newFrame != null ) {
-            newFrame.addToggleDialog(new NanoLogPanel());
+            NanoLogPanel panel = new NanoLogPanel();
+            newFrame.addToggleDialog(panel);
+            MapView.addLayerChangeListener(panel);
         }
     }
@@ -37,5 +43,13 @@
             JFileChooser fc = new JFileChooser();
             if( fc.showOpenDialog(Main.parent) == JFileChooser.APPROVE_OPTION ) {
-                // open layer, ok
+                try {
+                    List<NanoLogEntry> entries = NanoLogLayer.readNanoLog(fc.getSelectedFile());
+                    if( !entries.isEmpty() ) {
+                        NanoLogLayer layer = new NanoLogLayer(entries);
+                        Main.main.addLayer(layer);
+                    }
+                } catch( IOException ex ) {
+                    JOptionPane.showMessageDialog(Main.parent, tr("Could not read NanoLog file:") + "\n" + ex.getMessage());
+                }
             }
         }        
