Index: applications/editors/josm/plugins/agpifoj/CHANGELOG
===================================================================
--- applications/editors/josm/plugins/agpifoj/CHANGELOG	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/CHANGELOG	(revision 10122)
@@ -0,0 +1,1 @@
+01-15-2008 : Release of a first beta.
Index: applications/editors/josm/plugins/agpifoj/LICENSE
===================================================================
--- applications/editors/josm/plugins/agpifoj/LICENSE	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/LICENSE	(revision 10122)
@@ -0,0 +1,346 @@
+
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+	51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
Index: applications/editors/josm/plugins/agpifoj/README
===================================================================
--- applications/editors/josm/plugins/agpifoj/README	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/README	(revision 10122)
@@ -0,0 +1,57 @@
+AgPiFoj stands for 'Another geotag plug-in for Josm'.
+
+FEATURES
+
+- Access by a new menu item in the 'File' menu. This menu loads pictures and 
+  makes a new layer from them in the map view.
+- Displays the images in a ToggleDialog (so it appears as a panel on the right 
+  of the screen and can be shown/hidden with a click on a button of the left 
+  toolbar. It can be set in a separate window by clicking the sticky button)
+- Loads geotag data from exif or correlate pictures with GPS tracks.
+- Displays the pictures as a little camera icon in the map view (this improves 
+  the speed of loading large sets of pictures). The selected picture appears in
+  red. 
+- Easy zoom in/out of the image with the mouse wheel. Hability to move the 
+  image by clicking and/or dragging on it with mouse left button, or to select 
+  the part of the image to zoom in by dragging the right button.
+- Displays the altitude and speed of the photo when available from the GPS 
+  track.
+- Hability to synchronize a same set of photos with many GPS tracks (choose 
+  item 'Correlate to GPX' in the contextual menu of the layer). If a  picture 
+  set and a GPS track were badly time-synchronized, just load again the same 
+  GPX track on the layer, by specifying a different offset and/or timezone.
+- Adds a viewport to the left toolbar : with all these plug-ins that add buttons
+  to that toolbar, some of them became inaccessible. This adds some little 
+  arrows on top and bottom of the toolbar.
+
+NOTE 
+For the user who used to use the 'Import images' option on GPS layers, the 
+timezone is the opposite : it is greater than 0 when going to the east of
+Greenwich Meridian.
+
+INSTALL
+
+To install, put the agpifoj.jar in the JOSM plugin directory. Then in JOSM, 
+select the menu Edit / Preferences and the plugins tab. Check the agpifoj 
+plugin check-box, and restart JOSM. You'll seee the AgPiFoj menu item in 
+the 'File' menu.
+
+BUILD 
+
+The source code is in the agpifoj.jar : unzip it.
+Edit the build.xml to set the path to your josm-latest.jar as property.
+Run ant.
+The plugin jar file is in the dist directory.
+
+Tested on the latest JOSM version (build 521).
+
+CONTRIBUTION
+
+I got inspiration and some code from the Geotagged plugin (by Rob Neild) 
+and the core JOSM source code (by Immanuel Scholz and others). This plugin is 
+delivered under the GPL licence terms. It also uses the jpeg metadata 
+extraction code is from Drew Noakes (bundled with Josm).
+
+---
+
+Hope you'll find it useful.
Index: applications/editors/josm/plugins/agpifoj/build.xml
===================================================================
--- applications/editors/josm/plugins/agpifoj/build.xml	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/build.xml	(revision 10122)
@@ -0,0 +1,75 @@
+<project name="agpifoj" default="dist" basedir=".">
+
+  <!-- josm "user home" directory depends on the platform used (windows has a different place than unix/linux) -->
+  <property environment="env"/>
+  <condition property="josm.home.dir" value="${env.APPDATA}/JOSM" else="${user.home}/.josm">
+    <and>
+      <os family="windows"/>
+    </and>
+  </condition>
+
+  <!-- compilation properties -->
+  <property name="josm.build.dir"   value="../../core"/>
+  <property name="josm.plugins.dir" value="${josm.home.dir}/plugins"/>
+  <property name="josm"             location="../../core/dist/josm-custom.jar" />
+  <property name="plugin.build.dir" value="build"/>
+  <property name="plugin.dist.dir"  value="../../dist"/>
+  <property name="plugin.name"      value="${ant.project.name}"/>
+  <property name="plugin.jar"       value="../../dist/${plugin.name}.jar"/>
+
+  <property name="ant.build.javac.target" value="1.5"/>
+
+  <target name="dist" depends="compile">
+    <copy todir="build">
+      <fileset dir=".">
+        <include name="CHANGELOG"/>
+        <include name="LICENSE"/>
+        <include name="README" />
+      </fileset>
+    </copy>
+
+    <!-- images -->
+    <copy todir="build/images">
+      <fileset dir="images" />
+    </copy>
+
+    <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"/>
+
+    <jar destfile="${plugin.jar}" basedir="build">
+      <manifest>
+        <attribute name="Plugin-Class" value="org.openstreetmap.josm.plugins.agpifoj.AgpifojPlugin" />
+        <attribute name="Plugin-Description" value="An other geotag plugin for josm. Correlates pictures with GPS tracks, or import Exif geotagged pictures." />
+        <attribute name="Plugin-Early" value="false" />
+        <attribute name="Plugin-Version" value="${version.entry.commit.revision}"/>
+        <attribute name="Plugin-Date" value="${version.entry.commit.date}"/>
+      </manifest>
+    </jar>
+  </target>
+
+  <target name="compile" depends="init">
+    <echo message="creating ${plugin.jar}"/>
+    <javac srcdir="src" classpath="../../core/dist/josm-custom.jar" destdir="build">
+      <compilerarg value="-Xlint:deprecation"/>
+    </javac>
+  </target>
+
+  <target name="init">
+    <mkdir dir="${plugin.build.dir}" />
+  </target>
+
+  <target name="clean">
+    <delete dir="${plugin.build.dir}" />
+    <delete file="${plugin.jar}" />
+  </target>
+
+  <target name="install" depends="dist">
+    <copy file="${plugin.jar}" todir="${josm.plugins.dir}"/>
+  </target>
+</project>
Index: applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojDialog.java
===================================================================
--- applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojDialog.java	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojDialog.java	(revision 10122)
@@ -0,0 +1,166 @@
+// License: GPL. Copyright 2007 by Christian Gallioz (aka khris78)
+// Parts of code from Geotagged plugin (by Rob Neild) 
+// and the core JOSM source code (by Immanuel Scholz and others)
+
+package org.openstreetmap.josm.plugins.agpifoj;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import javax.swing.JToggleButton;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
+import org.openstreetmap.josm.plugins.agpifoj.AgpifojLayer.ImageEntry;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+public class AgpifojDialog extends ToggleDialog implements ActionListener {
+
+    private ImageDisplay imgDisplay = new ImageDisplay();
+    private boolean centerView = false;
+    
+    // Only one instance of thar class
+    static private AgpifojDialog INSTANCE = null;
+    
+    public static AgpifojDialog getInstance() {
+        if (INSTANCE == null) {
+            INSTANCE = new AgpifojDialog();
+        }
+        return INSTANCE;
+    }
+
+    private AgpifojDialog() {
+        super(tr("AgPifoJ - Geotagged pictures"), "agpifoj", tr("Display geotagged photos"), KeyEvent.VK_Y, 200);
+        
+        if (INSTANCE != null) {
+            throw new IllegalStateException("Agpifoj dialog should not be instanciated twice !");
+        }
+        
+        INSTANCE = this;
+        
+        JPanel content = new JPanel();
+        content.setLayout(new BorderLayout());
+
+        content.add(imgDisplay, BorderLayout.CENTER);
+        
+        JPanel buttons = new JPanel();
+        buttons.setLayout(new FlowLayout());
+        
+        JButton button;
+        
+        button = new JButton();
+        button.setIcon(ImageProvider.get("dialogs", "previous"));
+        button.setActionCommand("previous");
+        button.setToolTipText(tr("Previous"));
+        button.addActionListener(this);
+        buttons.add(button);
+        
+        button = new JButton();
+        button.setIcon(ImageProvider.get("dialogs", "delete"));
+        button.setActionCommand("remove");
+        button.setToolTipText(tr("Remove photo from layer"));
+        button.addActionListener(this);
+        buttons.add(button);
+        
+        button = new JButton();
+        button.setIcon(ImageProvider.get("dialogs", "next"));
+        button.setActionCommand("next");
+        button.setToolTipText(tr("Next"));
+        button.addActionListener(this);
+        buttons.add(button);
+        
+        JToggleButton tb = new JToggleButton();
+        tb.setIcon(ImageProvider.get("dialogs", "centreview"));
+        tb.setActionCommand("centre");
+        tb.setToolTipText(tr("Center view"));
+        tb.addActionListener(this);
+        buttons.add(tb);
+        
+        button = new JButton();
+        button.setIcon(ImageProvider.get("dialogs", "zoom-best-fit"));
+        button.setActionCommand("zoom");
+        button.setToolTipText(tr("Zoom best fit and 1:1"));
+        button.addActionListener(this);
+        buttons.add(button);
+        
+        content.add(buttons, BorderLayout.SOUTH);
+
+        add(content, BorderLayout.CENTER);
+        
+    }
+
+    public void actionPerformed(ActionEvent e) {
+        if ("next".equals(e.getActionCommand())) {
+            if (currentLayer != null) {
+                currentLayer.showNextPhoto();
+            }
+        } else if ("previous".equals(e.getActionCommand())) {
+            if (currentLayer != null) {
+                currentLayer.showPreviousPhoto();
+            }
+            
+        } else if ("centre".equals(e.getActionCommand())) {
+            centerView = ((JToggleButton) e.getSource()).isSelected();
+            if (centerView && currentEntry != null && currentEntry.pos != null) {
+                Main.map.mapView.zoomTo(currentEntry.pos, Main.map.mapView.getScale());
+            }
+            
+        } else if ("zoom".equals(e.getActionCommand())) {
+            imgDisplay.zoomBestFitOrOne();
+            
+        } else if ("remove".equals(e.getActionCommand())) {
+            if (currentLayer != null) {
+               currentLayer.removeCurrentPhoto();
+            }
+        }
+        
+    }
+
+    public static void showImage(AgpifojLayer layer, ImageEntry entry) {
+        if (INSTANCE == null) {
+            Main.main.map.addToggleDialog(new AgpifojDialog());
+        }
+        
+        if (INSTANCE != null) {
+            INSTANCE.displayImage(layer, entry);
+        }
+        
+    }
+
+    private AgpifojLayer currentLayer = null;
+    private ImageEntry currentEntry = null;
+    
+    public void displayImage(AgpifojLayer layer, ImageEntry entry) {
+        synchronized(this) {
+            if (currentLayer == layer && currentEntry == entry) {
+                repaint();
+                return;
+            }
+        
+            if (centerView && entry != null && entry.pos != null) {
+                Main.map.mapView.zoomTo(entry.pos, Main.map.mapView.getScale());
+            }
+
+            currentLayer = layer;
+            currentEntry = entry;
+        }
+        
+        imgDisplay.setImage(entry != null ? entry.file : null);
+        StringBuffer osd = new StringBuffer(entry.file.getName());
+        if (entry.elevation != null) {
+            osd.append(tr("\nAltitude: ")).append(entry.elevation.longValue()).append(" m");
+        }
+        if (entry.speed != null) {
+            osd.append("\n").append((long) (3.6 * entry.speed)).append(tr(" km/h"));
+        }
+        imgDisplay.setOsdText(osd.toString());
+    }
+
+}
Index: applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojLayer.java
===================================================================
--- applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojLayer.java	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojLayer.java	(revision 10122)
@@ -0,0 +1,450 @@
+// License: GPL. Copyright 2007 by Christian Gallioz (aka khris78)
+// Parts of code from Geotagged plugin (by Rob Neild) 
+// and the core JOSM source code (by Immanuel Scholz and others)
+
+package org.openstreetmap.josm.plugins.agpifoj;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.File;
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import javax.swing.Icon;
+import javax.swing.JMenuItem;
+import javax.swing.JSeparator;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.RenameLayerAction;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.tools.ExifReader;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+import com.drew.imaging.jpeg.JpegMetadataReader;
+import com.drew.lang.Rational;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.exif.GpsDirectory;
+
+public class AgpifojLayer extends Layer {
+
+    List<ImageEntry> data;
+    
+    private Icon icon = ImageProvider.get("dialogs/agpifoj-marker");
+    private Icon selectedIcon = ImageProvider.get("dialogs/agpifoj-marker-selected");
+    
+    private int currentPhoto = -1;
+    
+    /*
+     * Stores info about each image
+     */
+
+    static final class ImageEntry implements Comparable<ImageEntry> {
+        File file;
+        Date time;
+        LatLon exifCoor;
+        LatLon coor;
+        EastNorth pos;
+        /** Speed in meter per second */
+        Double speed;
+        /** Elevation (altitude) in meters */
+        Double elevation;
+
+        public int compareTo(ImageEntry image) {
+            if (time != null && image.time != null) {
+                return time.compareTo(image.time);
+            } else if (time == null && image.time == null) {
+                return 0;
+            } else if (time == null) {
+                return -1;
+            } else {
+                return 1;
+            }
+        }
+    }
+
+    private static final class Loader extends PleaseWaitRunnable {
+
+        private boolean cancelled = false;
+        private AgpifojLayer layer;
+        private final Collection<File> files;
+
+        public Loader(Collection<File> files) {
+            super(tr("Extracting GPS locations from EXIF"));
+            this.files = files;
+        }
+
+        @Override protected void realRun() throws IOException {
+
+            Main.pleaseWaitDlg.currentAction.setText(tr("Read photos..."));
+
+            // read the image files
+            ArrayList<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
+
+            int progress = 0;
+            Main.pleaseWaitDlg.progress.setMaximum(files.size());
+            Main.pleaseWaitDlg.progress.setValue(progress);
+            
+            for (File f : files) {
+
+                if (cancelled) {
+                    break;
+                }
+
+                Main.pleaseWaitDlg.currentAction.setText(tr("Reading {0}...", f.getName()));
+                Main.pleaseWaitDlg.progress.setValue(progress++);
+
+                ImageEntry e = new ImageEntry();
+
+                // Changed to silently cope with no time info in exif. One case
+                // of person having time that couldn't be parsed, but valid GPS info
+
+                try {
+                    e.time = ExifReader.readTime(f);
+                } catch (ParseException e1) {
+                    e.time = null;
+                }
+                e.file = f;
+                extractExif(e);
+                data.add(e);
+            }
+            layer = new AgpifojLayer(data);
+        }
+
+        @Override protected void finish() {
+            if (layer != null) {
+                Main.main.addLayer(layer);
+                layer.hook_up_mouse_events(); // Main.map.mapView should exist
+                                              // now. Can add mouse lisener
+
+                if (! cancelled && layer.data.size() > 0) {
+                    boolean noGeotagFound = true;
+                    for (ImageEntry e : layer.data) {
+                        if (e.pos != null) {
+                            noGeotagFound = false;
+                        }
+                    }
+                    if (noGeotagFound) {
+                        new CorrelateGpxWithImages(layer).actionPerformed(null);
+                    }
+                }
+            }
+        }
+
+        @Override protected void cancel() {
+            cancelled = true;
+        }
+    }
+
+    public static void create(Collection<File> files) {
+        Loader loader = new Loader(files);
+        Main.worker.execute(loader);
+    }
+
+    private AgpifojLayer(final List<ImageEntry> data) {
+
+        super(tr("Geotagged Images"));
+
+        Collections.sort(data);
+        this.data = data;
+    }
+
+    @Override
+    public Icon getIcon() {
+        return ImageProvider.get("dialogs/agpifoj");
+    }
+
+    @Override
+    public Object getInfoComponent() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public Component[] getMenuEntries() {
+        
+        JMenuItem correlateItem = new JMenuItem(tr("Correlate to GPX"), ImageProvider.get("dialogs/gpx2img"));
+        correlateItem.addActionListener(new CorrelateGpxWithImages(this));
+
+        return new Component[] {
+                new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)),
+                new JMenuItem(new LayerListDialog.DeleteLayerAction(this)),
+                new JMenuItem(new RenameLayerAction(null, this)),
+                new JSeparator(),
+                correlateItem
+                };
+    }
+
+    @Override
+    public String getToolTipText() {
+        int i = 0;
+        for (ImageEntry e : data)
+            if (e.pos != null)
+                i++;
+        return data.size() + " " + trn("image", "images", data.size())
+                + " loaded. " + tr("{0} were found to be gps tagged.", i);
+    }
+
+    @Override
+    public boolean isMergable(Layer other) {
+        return other instanceof AgpifojLayer;
+    }
+
+    @Override
+    public void mergeFrom(Layer from) {
+        AgpifojLayer l = (AgpifojLayer) from;
+
+        ImageEntry selected = null; 
+        if (l.currentPhoto >= 0) {
+            selected = l.data.get(l.currentPhoto);
+        }
+        
+        data.addAll(l.data);
+        Collections.sort(data);
+        
+        // Supress the double photos.
+        if (data.size() > 1) {
+            ImageEntry cur;
+            ImageEntry prev = data.get(data.size() - 1);
+            for (int i = data.size() - 2; i >= 0; i--) {
+                cur = data.get(i);
+                if (cur.file.equals(prev.file)) {
+                    data.remove(i);
+                } else {
+                    prev = cur;
+                }
+            }
+        }
+        
+        if (selected != null) {
+            for (int i = 0; i < data.size() ; i++) {
+                if (data.get(i) == selected) {
+                    currentPhoto = i;
+                    AgpifojDialog.showImage(AgpifojLayer.this, data.get(i));
+                    break;
+                }
+            }
+        }
+        
+        name = l.name;
+        
+    }
+
+    @Override
+    public void paint(Graphics g, MapView mv) {
+
+        int iconWidth = icon.getIconWidth() / 2;
+        int iconHeight = icon.getIconHeight() / 2;
+        
+        for (ImageEntry e : data) {
+            if (e.pos != null) {
+                Point p = mv.getPoint(e.pos);
+
+                Rectangle r = new Rectangle(p.x - iconWidth,
+                                            p.y - iconHeight,
+                                            icon.getIconWidth(), 
+                                            icon.getIconHeight());
+                icon.paintIcon(mv, g, r.x, r.y);
+            }
+        }
+        
+        // Draw the selection on top of the other pictures.
+        if (currentPhoto >= 0 && currentPhoto < data.size()) {
+            ImageEntry e = data.get(currentPhoto);
+
+            if (e.pos != null) {
+                Point p = mv.getPoint(e.pos);
+
+                Rectangle r = new Rectangle(p.x - selectedIcon.getIconWidth() / 2,
+                                            p.y - selectedIcon.getIconHeight() / 2,
+                                            selectedIcon.getIconWidth(), 
+                                            selectedIcon.getIconHeight());
+                selectedIcon.paintIcon(mv, g, r.x, r.y);
+            }
+        }
+    }
+
+    @Override
+    public void visitBoundingBox(BoundingXYVisitor v) {
+        for (ImageEntry e : data)
+            v.visit(e.pos);
+    }
+
+    /*
+     * Extract gps from image exif
+     * 
+     * If successful, fills in the LatLon and EastNorth attributes of passed in
+     * image;
+     */
+
+    private static void extractExif(ImageEntry e) {
+
+        try {
+            int deg;
+            float min, sec;
+            double lon, lat;
+
+            Metadata metadata = JpegMetadataReader.readMetadata(e.file);
+            Directory dir = metadata.getDirectory(GpsDirectory.class);
+
+            // longitude
+
+            Rational[] components = dir
+                    .getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE);
+
+            deg = components[0].intValue();
+            min = components[1].floatValue();
+            sec = components[2].floatValue();
+
+            lon = (deg + (min / 60) + (sec / 3600));
+
+            if (dir.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF).charAt(0) == 'W')
+                lon = -lon;
+
+            // latitude
+
+            components = dir.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE);
+
+            deg = components[0].intValue();
+            min = components[1].floatValue();
+            sec = components[2].floatValue();
+
+            lat = (deg + (min / 60) + (sec / 3600));
+
+            if (dir.getString(GpsDirectory.TAG_GPS_LATITUDE_REF).charAt(0) == 'S')
+                lat = -lat;
+
+            // Store values
+
+            e.coor = new LatLon(lat, lon);
+            e.exifCoor = e.coor;
+            e.pos = Main.proj.latlon2eastNorth(e.coor);
+
+        } catch (Exception p) {
+            e.coor = null;
+            e.pos = null;
+        }
+    }
+    
+    public void showNextPhoto() {
+        if (data != null && data.size() > 0) {
+            currentPhoto++;
+            if (currentPhoto >= data.size()) {
+                currentPhoto = data.size() - 1;
+            }
+            AgpifojDialog.showImage(this, data.get(currentPhoto));
+        } else {
+            currentPhoto = -1;
+        }
+        Main.main.map.repaint();
+    }
+    
+    public void showPreviousPhoto() {
+        if (data != null && data.size() > 0) {
+            currentPhoto--;
+            if (currentPhoto < 0) {
+                currentPhoto = 0;
+            }
+            AgpifojDialog.showImage(this, data.get(currentPhoto));
+        } else {
+            currentPhoto = -1;
+        }
+        Main.main.map.repaint();
+    }
+    
+    public void removeCurrentPhoto() {
+        if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) {
+            data.remove(currentPhoto);
+            if (currentPhoto >= data.size()) {
+                currentPhoto = data.size() - 1;
+            }
+            if (currentPhoto >= 0) {
+                AgpifojDialog.showImage(this, data.get(currentPhoto));
+            } else {
+                AgpifojDialog.showImage(this, null);
+            }
+        }
+        Main.main.map.repaint();
+    }
+    
+    private MouseAdapter mouseAdapter = null;
+
+    private void hook_up_mouse_events() {
+        mouseAdapter = new MouseAdapter() {
+            @Override public void mousePressed(MouseEvent e) {
+
+                if (e.getButton() != MouseEvent.BUTTON1) {
+                    return;
+                }
+                if (visible)
+                    Main.map.mapView.repaint();
+            }
+
+            @Override public void mouseReleased(MouseEvent ev) {
+                if (ev.getButton() != MouseEvent.BUTTON1) {
+                    return;
+                }
+                if (!visible) {
+                    return;
+                }
+                for (int i = data.size() - 1; i >= 0; --i) {
+                    ImageEntry e = data.get(i);
+                    if (e.pos == null)
+                        continue;
+                    Point p = Main.map.mapView.getPoint(e.pos);
+                    Rectangle r = new Rectangle(p.x - icon.getIconWidth() / 2, 
+                                                p.y - icon.getIconHeight() / 2, 
+                                                icon.getIconWidth(), 
+                                                icon.getIconHeight());
+                    if (r.contains(ev.getPoint())) {
+                        currentPhoto = i;
+                        AgpifojDialog.showImage(AgpifojLayer.this, e);
+                        Main.main.map.repaint();
+                        break;
+                    }
+                }
+                Main.map.mapView.repaint();
+            }
+        };
+        Main.map.mapView.addMouseListener(mouseAdapter);
+        Layer.listeners.add(new LayerChangeListener() {
+            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+                if (newLayer == AgpifojLayer.this && currentPhoto >= 0) {
+                    Main.main.map.repaint();
+                    AgpifojDialog.showImage(AgpifojLayer.this, data.get(currentPhoto));
+                }
+            }
+
+            public void layerAdded(Layer newLayer) {
+            }
+
+            public void layerRemoved(Layer oldLayer) {
+                if (oldLayer == AgpifojLayer.this) {
+                    Main.map.mapView.removeMouseListener(mouseAdapter);
+                    currentPhoto = -1;
+                    data.clear();
+                    data = null;
+                }
+            }
+        });
+    }
+
+}
Index: applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojPlugin.java
===================================================================
--- applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojPlugin.java	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/AgpifojPlugin.java	(revision 10122)
@@ -0,0 +1,141 @@
+// License: GPL. Copyright 2007 by Christian Gallioz (aka khris78)
+// Parts of code from Geotagged plugin (by Rob Neild) 
+// and the core JOSM source code (by Immanuel Scholz and others)
+
+package org.openstreetmap.josm.plugins.agpifoj;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JFileChooser;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JToggleButton;
+import javax.swing.JToolBar;
+import javax.swing.filechooser.FileFilter;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.plugins.Plugin;
+
+public class AgpifojPlugin extends Plugin {
+    
+    private class Action extends JosmAction {
+
+        public Action() {
+            super(tr("Open images with AgPifoJ"),
+                  "agpifoj-open",
+                  tr("Load set of images as a new layer."),
+                  0, 0, false);
+        }
+
+        public void actionPerformed(ActionEvent e) {
+
+            JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
+            fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+            fc.setMultiSelectionEnabled(true);
+            fc.setAcceptAllFileFilterUsed(false);
+            fc.setFileFilter(
+                    new FileFilter() {
+                        @Override public boolean accept(File f) {
+                            return f.isDirectory()
+                                    || f.getName().toLowerCase().endsWith(".jpg");
+                        }
+
+                        @Override public String getDescription() {
+                            return tr("JPEG images (*.jpg)");
+                        }
+                    });
+            
+            fc.showOpenDialog(Main.parent);
+            
+            File[] sel = fc.getSelectedFiles();
+            if (sel == null || sel.length == 0) {
+                return;
+            }
+            
+            List<File> files = new ArrayList<File>();
+            addRecursiveFiles(files, sel);
+            Main.pref.put("tagimages.lastdirectory", fc.getCurrentDirectory().getPath());
+            
+            AgpifojLayer.create(files);
+        }
+
+        private void addRecursiveFiles(List<File> files, File[] sel) {
+            for (File f : sel) {
+                if (f.isDirectory()) {
+                    addRecursiveFiles(files, f.listFiles());
+                } else if (f.getName().toLowerCase().endsWith(".jpg")) {
+                    files.add(f);
+                }
+            }
+        }
+    }
+
+    public AgpifojPlugin() {
+
+        JMenuBar menu = Main.main.menu;
+        JMenu view = null;
+        JMenuItem agpifojMenu = new JMenuItem(new Action());
+
+        for (int i = 0; i < menu.getMenuCount(); ++i) {
+            if (menu.getMenu(i) != null
+                    && tr("File").equals(menu.getMenu(i).getText())) {
+                view = menu.getMenu(i);
+                break;
+            }
+        }
+
+        if (view != null) {
+            view.insert(agpifojMenu, 2);
+        
+        } else if (menu.getMenuCount() > 0) {
+            view = menu.getMenu(0);
+            view.insert(agpifojMenu, 0);
+        }
+
+        agpifojMenu.setVisible(true);
+    }
+
+    /**
+     * Called after Main.mapFrame is initalized. (After the first data is loaded).
+     * You can use this callback to tweak the newFrame to your needs, as example install
+     * an alternative Painter.
+     */
+    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+        if (newFrame != null) {
+            if (newFrame.getLayout() instanceof BorderLayout) {
+                newFrame.remove(newFrame.toolBarActions);
+                newFrame.add(new ScrollViewport(newFrame.toolBarActions, ScrollViewport.VERTICAL_DIRECTION), 
+                             BorderLayout.WEST);
+                newFrame.repaint();
+            }
+            
+            AgpifojDialog dialog = AgpifojDialog.getInstance();
+            newFrame.addToggleDialog(dialog);
+        
+            boolean found = false;
+            for (Layer layer : newFrame.mapView.getAllLayers()) {
+                if (layer instanceof AgpifojLayer) {
+                    found = true;
+                    break;
+                }
+            }
+            JToolBar tb = newFrame.toolBarActions;
+            ((JToggleButton) tb.getComponent(tb.getComponentCount() - 1)).getModel().setSelected(found);
+        
+        } else {
+            AgpifojDialog.getInstance().displayImage(null, null);
+        }
+    }
+
+
+}
Index: applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/CorrelateGpxWithImages.java
===================================================================
--- applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/CorrelateGpxWithImages.java	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/CorrelateGpxWithImages.java	(revision 10122)
@@ -0,0 +1,879 @@
+// License: GPL. Copyright 2007 by Christian Gallioz (aka khris78)
+// Parts of code from Geotagged plugin (by Rob Neild) 
+// and the core JOSM source code (by Immanuel Scholz and others)
+
+package org.openstreetmap.josm.plugins.agpifoj;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.Vector;
+import java.util.zip.GZIPInputStream;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.filechooser.FileFilter;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.data.gpx.GpxTrack;
+import org.openstreetmap.josm.data.gpx.WayPoint;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.io.GpxReader;
+import org.openstreetmap.josm.plugins.agpifoj.AgpifojLayer.ImageEntry;
+import org.openstreetmap.josm.tools.ExifReader;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.PrimaryDateParser;
+import org.xml.sax.SAXException;
+
+/** This class displays the window to select the GPX file and the offset (timezone + delta). 
+ * Then it correlates the images of the layer with that GPX file.
+ */
+public class CorrelateGpxWithImages implements ActionListener {
+
+    private static List<GpxData> loadedGpxData = new ArrayList<GpxData>();
+
+    public static class CorrelateParameters {
+        GpxData gpxData;
+        float timezone;
+        long offset;
+    }
+    
+    AgpifojLayer yLayer = null;
+    
+    private static class GpxDataWrapper {
+        String name;
+        GpxData data;
+        File file;
+        
+        public GpxDataWrapper(String name, GpxData data, File file) {
+            this.name = name;
+            this.data = data; 
+            this.file = file;
+        }
+        
+        public String toString() {
+            return name;
+        }
+    }
+    
+    Vector gpxLst = new Vector();
+    JPanel panel = null;
+    JComboBox cbGpx = null;
+    JTextField tfTimezone = null;
+    JTextField tfOffset = null;
+    JRadioButton rbAllImg = null;
+    JRadioButton rbUntaggedImg = null;
+    JRadioButton rbNoExifImg = null;
+    
+    /** This class is called when the user doesn't find the GPX file he needs in the files that have 
+     * been loaded yet. It displays a FileChooser dialog to select the GPX file to be loaded.
+     */ 
+    private class LoadGpxDataActionListener implements ActionListener {
+        
+        public void actionPerformed(ActionEvent arg0) {
+            JFileChooser fc = new JFileChooser(Main.pref.get("lastDirectory"));
+            fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+            fc.setAcceptAllFileFilterUsed(false);
+            fc.setMultiSelectionEnabled(false);
+            fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+            fc.setFileFilter(new FileFilter(){
+                @Override public boolean accept(File f) {
+                    return (f.isDirectory()
+                            || f .getName().toLowerCase().endsWith(".gpx") 
+                            || f.getName().toLowerCase().endsWith(".gpx.gz"));
+                }
+                @Override public String getDescription() {
+                    return tr("GPX tracks (*.gpx, *.gpx.gz)");
+                }
+            });
+            fc.showOpenDialog(Main.parent);
+            File sel = fc.getSelectedFile();
+            if (sel == null)
+                return;
+            
+            try {
+                panel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+                
+                Main.pref.put("lastDirectory", sel.getPath());
+                
+                for (int i = gpxLst.size() - 1 ; i >= 0 ; i--) {
+                    if (gpxLst.get(i) instanceof GpxDataWrapper) {
+                        GpxDataWrapper wrapper = (GpxDataWrapper) gpxLst.get(i); 
+                        if (sel.equals(wrapper.file)) {
+                            cbGpx.setSelectedIndex(i);
+                            if (!sel.getName().equals(wrapper.name)) {
+                                JOptionPane.showMessageDialog(Main.parent, 
+                                        tr("File {0} is loaded yet under the name \"{1}\"", sel.getName(), wrapper.name));
+                            }
+                            return;
+                        }
+                    }
+                }
+                GpxData data = null;
+                try {
+                    InputStream iStream;
+                    if (sel.getName().toLowerCase().endsWith(".gpx.gz")) {
+                        iStream = new GZIPInputStream(new FileInputStream(sel));
+                    } else {
+                        iStream = new FileInputStream(sel); 
+                    }
+                    data = new GpxReader(iStream, sel).data;
+                    data.storageFile = sel;
+               
+                } catch (SAXException x) {
+                    x.printStackTrace();
+                    JOptionPane.showMessageDialog(Main.parent, tr("Error while parsing {0}",sel.getName())+": "+x.getMessage());
+                    return;
+                } catch (IOException x) {
+                    x.printStackTrace();
+                    JOptionPane.showMessageDialog(Main.parent, tr("Could not read \"{0}\"",sel.getName())+"\n"+x.getMessage());
+                    return;
+                }
+                
+                loadedGpxData.add(data);
+                if (gpxLst.get(0) instanceof String) {
+                    gpxLst.remove(0);
+                }
+                gpxLst.add(new GpxDataWrapper(sel.getName(), data, sel));
+                cbGpx.setSelectedIndex(cbGpx.getItemCount() - 1);
+            } finally {
+                panel.setCursor(Cursor.getDefaultCursor());
+            }
+        }
+    }
+    
+    /** This action listener is called when the user has a photo of the time of his GPS receptor. It 
+     * displays the list of photos of the layer, and upon selection displays the selected photo.
+     * From that photo, the user can key in the time of the GPS. 
+     * Then values of timezone and delta are set. 
+     * @author chris
+     *
+     */
+    private class SetOffsetActionListener implements ActionListener {
+        JPanel panel;
+        JLabel lbExifTime;
+        JTextField tfGpsTime;
+        JComboBox cbTimezones;
+        ImageDisplay imgDisp;
+        JList imgList;
+        
+        public void actionPerformed(ActionEvent arg0) {
+            SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
+
+            panel = new JPanel();
+            panel.setLayout(new BorderLayout());
+            panel.add(new JLabel(tr("<html>Take a photo of your GPS receptor while it displays the time.<br>"
+                                    + "Display that photo here.<br>"
+                                    + "And then, simply capture the time you read on the photo and select a timezone<hr></html>")), 
+                                    BorderLayout.NORTH);
+            
+            imgDisp = new ImageDisplay();
+            imgDisp.setPreferredSize(new Dimension(300, 225));
+            panel.add(imgDisp, BorderLayout.CENTER);
+            
+            JPanel panelTf = new JPanel();
+            panelTf.setLayout(new GridBagLayout());
+            
+            GridBagConstraints gc = new GridBagConstraints();
+            gc.gridx = gc.gridy = 0;
+            gc.gridwidth = gc.gridheight = 1;
+            gc.weightx = gc.weighty = 0.0;
+            gc.fill = GridBagConstraints.NONE;
+            gc.anchor = GridBagConstraints.WEST;
+            panelTf.add(new JLabel(tr("Photo time (from exif) :")), gc);
+
+            lbExifTime = new JLabel();
+            gc.gridx = 1;
+            gc.weightx = 1.0;
+            gc.fill = GridBagConstraints.HORIZONTAL;
+            gc.gridwidth = 2;
+            panelTf.add(lbExifTime, gc);
+            
+            gc.gridx = 0;
+            gc.gridy = 1;
+            gc.gridwidth = gc.gridheight = 1;
+            gc.weightx = gc.weighty = 0.0;
+            gc.fill = GridBagConstraints.NONE;
+            gc.anchor = GridBagConstraints.WEST;
+            panelTf.add(new JLabel(tr("Gps time (read from the above photo) : ")), gc);
+
+            tfGpsTime = new JTextField();
+            tfGpsTime.setEnabled(false);
+            tfGpsTime.setMinimumSize(new Dimension(150, tfGpsTime.getMinimumSize().height));
+            gc.gridx = 1;
+            gc.weightx = 1.0;
+            gc.fill = GridBagConstraints.HORIZONTAL;
+            panelTf.add(tfGpsTime, gc);
+
+            gc.gridx = 2;
+            gc.weightx = 0.2;
+            panelTf.add(new JLabel(tr(" [dd/mm/yyyy hh:mm:ss]")), gc);
+            
+            gc.gridx = 0;
+            gc.gridy = 2;
+            gc.gridwidth = gc.gridheight = 1;
+            gc.weightx = gc.weighty = 0.0;
+            gc.fill = GridBagConstraints.NONE;
+            gc.anchor = GridBagConstraints.WEST;
+            panelTf.add(new JLabel(tr("I'm in the timezone of : ")), gc);
+            
+            Vector vtTimezones = new Vector<String>();
+            String[] tmp = TimeZone.getAvailableIDs();
+            
+            for (String tzStr : tmp) {
+                TimeZone tz = TimeZone.getTimeZone(tzStr);
+                 
+                String tzDesc = new StringBuffer(tzStr).append(" (")
+                                        .append(formatTimezone(tz.getRawOffset() / 3600000.0))
+                                        .append(')').toString();
+                vtTimezones.add(tzDesc);
+            }
+            
+            Collections.sort(vtTimezones);
+            
+            cbTimezones = new JComboBox(vtTimezones);
+            
+            String tzId = Main.pref.get("tagimages.timezoneid", "");
+            TimeZone defaultTz;
+            if (tzId.length() == 0) {
+                defaultTz = TimeZone.getDefault();
+            } else {
+                defaultTz = TimeZone.getTimeZone(tzId);
+            }
+            
+            cbTimezones.setSelectedItem(new StringBuffer(defaultTz.getID()).append(" (")
+                    .append(formatTimezone(defaultTz.getRawOffset() / 3600000.0))
+                    .append(')').toString());
+            
+            gc.gridx = 1;
+            gc.weightx = 1.0;
+            gc.gridwidth = 2;
+            gc.fill = GridBagConstraints.HORIZONTAL;
+            panelTf.add(cbTimezones, gc);
+            
+            panel.add(panelTf, BorderLayout.SOUTH);
+
+            JPanel panelLst = new JPanel();
+            panelLst.setLayout(new BorderLayout());
+            
+            imgList = new JList(new AbstractListModel() {
+                public Object getElementAt(int i) {
+                    return yLayer.data.get(i).file.getName();
+                }
+
+                public int getSize() {
+                    return yLayer.data.size();
+                }
+            });
+            imgList.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+            imgList.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+                public void valueChanged(ListSelectionEvent arg0) {
+                    int index = imgList.getSelectedIndex();
+                    imgDisp.setImage(yLayer.data.get(index).file);
+                    Date date = yLayer.data.get(index).time;
+                    if (date != null) {
+                        lbExifTime.setText(new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(date));                    
+                        tfGpsTime.setText(new SimpleDateFormat("dd/MM/yyyy ").format(date));
+                        tfGpsTime.setCaretPosition(tfGpsTime.getText().length());
+                        tfGpsTime.setEnabled(true);
+                    } else {                        
+                        lbExifTime.setText(tr("No date"));
+                        tfGpsTime.setText("");
+                        tfGpsTime.setEnabled(false);
+                    }
+                }
+                
+            });
+            panelLst.add(new JScrollPane(imgList), BorderLayout.CENTER);
+            
+            JButton openButton = new JButton(tr("Open an other photo"));
+            openButton.addActionListener(new ActionListener() {
+
+                public void actionPerformed(ActionEvent arg0) {
+                    JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
+                    fc.setAcceptAllFileFilterUsed(false);
+                    fc.setMultiSelectionEnabled(false);
+                    fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+                    fc.setFileFilter(new FileFilter(){
+                        @Override public boolean accept(File f) {
+                            return (f.isDirectory()
+                                    || f .getName().toLowerCase().endsWith(".jpg") 
+                                    || f.getName().toLowerCase().endsWith(".jpeg"));
+                        }
+                        @Override public String getDescription() {
+                            return tr("Images (*.jpg)");
+                        }
+                    });
+                    fc.showOpenDialog(Main.parent);
+                    File sel = fc.getSelectedFile();
+                    if (sel == null) {
+                        return;
+                    }
+
+                    imgDisp.setImage(sel);
+
+                    Date date = null;
+                    try {
+                        date = ExifReader.readTime(sel);
+                    } catch (Exception e) {
+                    }
+                    if (date != null) {
+                        lbExifTime.setText(new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(date));                    
+                        tfGpsTime.setText(new SimpleDateFormat("dd/MM/yyyy ").format(date));
+                        tfGpsTime.setEnabled(true);
+                    } else {                        
+                        lbExifTime.setText(tr("No date"));
+                        tfGpsTime.setText("");
+                        tfGpsTime.setEnabled(false);
+                    }
+                }
+            });
+            panelLst.add(openButton, BorderLayout.PAGE_END);
+            
+            panel.add(panelLst, BorderLayout.LINE_START);
+            
+            boolean isOk = false;
+            while (! isOk) {
+                int answer = JOptionPane.showConfirmDialog(Main.parent, panel, tr("Synchronize time from a photo of the GPS receptor"), JOptionPane.OK_CANCEL_OPTION);
+                if (answer == JOptionPane.CANCEL_OPTION) {
+                    return;
+                }
+
+                long delta; 
+                   
+                try {
+                    delta = dateFormat.parse(lbExifTime.getText()).getTime() 
+                            - dateFormat.parse(tfGpsTime.getText()).getTime();
+                } catch(ParseException e) {
+                    JOptionPane.showMessageDialog(Main.parent, tr("Error while parsing the date.\n" 
+                            + "Please use the requested format"), 
+                            tr("Invalid date"), JOptionPane.ERROR_MESSAGE );
+                    continue;
+                }
+                
+                String selectedTz = (String) cbTimezones.getSelectedItem();
+                int pos = selectedTz.lastIndexOf('(');
+                tzId = selectedTz.substring(0, pos - 1);
+                String tzValue = selectedTz.substring(pos + 1, selectedTz.length() - 1); 
+                
+                Main.pref.put("tagimages.timezoneid", tzId);
+                tfOffset.setText(Long.toString(delta / 1000));
+                tfTimezone.setText(tzValue);
+                
+                isOk = true;
+                
+            }
+            
+        }
+    }
+    
+    public CorrelateGpxWithImages(AgpifojLayer layer) {
+        this.yLayer = layer;
+    }
+
+    public void actionPerformed(ActionEvent arg0) {
+        // Construct the list of loaded GPX tracks
+        Collection<Layer> layerLst = Main.main.map.mapView.getAllLayers();
+        Iterator<Layer> iterLayer = layerLst.iterator();
+        while (iterLayer.hasNext()) {
+            Layer cur = iterLayer.next();
+            if (cur instanceof GpxLayer) {
+                gpxLst.add(new GpxDataWrapper(((GpxLayer) cur).name, 
+                                              ((GpxLayer) cur).data, 
+                                              ((GpxLayer) cur).data.storageFile)); 
+            }
+        }
+        for (GpxData data : loadedGpxData) {
+            gpxLst.add(new GpxDataWrapper(data.storageFile.getName(), 
+                                          data,
+                                          data.storageFile)); 
+        }
+        
+        if (gpxLst.size() == 0) {
+            gpxLst.add(tr("<No GPX track loaded yet>"));
+        }
+        
+        JPanel panelCb = new JPanel();
+        panelCb.setLayout(new FlowLayout());
+        
+        panelCb.add(new JLabel(tr("GPX track : ")));
+        
+        cbGpx = new JComboBox(gpxLst);
+        panelCb.add(cbGpx);
+        
+        JButton buttonOpen = new JButton(tr("Open an other GPXtrace"));
+        buttonOpen.setIcon(ImageProvider.get("agpifoj-open"));
+        buttonOpen.addActionListener(new LoadGpxDataActionListener());
+        
+        panelCb.add(buttonOpen);
+        
+        JPanel panelTf = new JPanel();
+        panelTf.setLayout(new GridBagLayout());
+        
+        GridBagConstraints gc = new GridBagConstraints();
+        gc.anchor = GridBagConstraints.WEST;
+        
+        gc.gridx = gc.gridy = 0;
+        gc.gridwidth = gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.weightx = gc.weighty = 0.0;
+        panelTf.add(new JLabel(tr("Timezone : ")), gc);
+
+        float gpstimezone = Float.parseFloat(Main.pref.get("tagimages.doublegpstimezone", "0.0"));
+        if (gpstimezone == 0.0) {
+            gpstimezone = - Long.parseLong(Main.pref.get("tagimages.gpstimezone", "0"));
+        }
+        tfTimezone = new JTextField();
+        tfTimezone.setText(formatTimezone(gpstimezone));
+
+        gc.gridx = 1;
+        gc.gridy = 0;
+        gc.gridwidth = gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        panelTf.add(tfTimezone, gc);
+        
+        gc.gridx = 0;
+        gc.gridy = 1;
+        gc.gridwidth = gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.weightx = gc.weighty = 0.0;
+        panelTf.add(new JLabel(tr("Offset :")), gc);
+
+        long delta = Long.parseLong(Main.pref.get("tagimages.delta", "0")) / 1000;
+        tfOffset = new JTextField();
+        tfOffset.setText(Long.toString(delta));
+        gc.gridx = gc.gridy = 1;
+        gc.gridwidth = gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        panelTf.add(tfOffset, gc);
+
+        JButton buttonViewGpsPhoto = new JButton(tr("<html>I can take a picture of my GPS receptor.<br>" 
+                                                    + "Can this help ? </html>"));
+        buttonViewGpsPhoto.addActionListener(new SetOffsetActionListener());
+        gc.gridx = 2;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 2;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.weightx = 0.5;
+        gc.weighty = 1.0;
+        panelTf.add(buttonViewGpsPhoto, gc);
+        
+        gc.gridx = 0;
+        gc.gridy = 2;
+        gc.gridwidth = gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.weightx = gc.weighty = 0.0;
+        panelTf.add(new JLabel(tr("Update position for : ")), gc);
+        
+        gc.gridx = 1;
+        gc.gridy = 2;
+        gc.gridwidth = 2;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        rbAllImg = new JRadioButton(tr("All images"));
+        panelTf.add(rbAllImg, gc);
+        
+        gc.gridx = 1;
+        gc.gridy = 3;
+        gc.gridwidth = 2;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        rbNoExifImg = new JRadioButton(tr("Images with no exif position"));
+        panelTf.add(rbNoExifImg, gc);
+        
+        gc.gridx = 1;
+        gc.gridy = 4;
+        gc.gridwidth = 2;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        rbUntaggedImg = new JRadioButton(tr("Not yet tagged images"));
+        panelTf.add(rbUntaggedImg, gc);
+        
+        ButtonGroup group = new ButtonGroup();
+        group.add(rbAllImg);
+        group.add(rbNoExifImg);
+        group.add(rbUntaggedImg);
+        
+        rbUntaggedImg.setSelected(true);
+        
+        panel = new JPanel();
+        panel.setLayout(new BorderLayout());
+        
+        panel.add(panelCb, BorderLayout.PAGE_START);
+        panel.add(panelTf, BorderLayout.CENTER);
+
+        boolean isOk = false;
+        GpxDataWrapper selectedGpx = null;
+        while (! isOk) {
+            int answer = JOptionPane.showConfirmDialog(Main.parent, panel, tr("Correlate images with GPX track"), JOptionPane.OK_CANCEL_OPTION);
+            if (answer == JOptionPane.CANCEL_OPTION) {
+                return;
+            }
+            // Check the selected values
+            Object item = cbGpx.getSelectedItem();
+
+            if (item == null || ! (item instanceof GpxDataWrapper)) {
+                JOptionPane.showMessageDialog(Main.parent, tr("You should select a GPX track"), 
+                                              tr("No selected GPX track"), JOptionPane.ERROR_MESSAGE );
+                continue;
+            }
+            selectedGpx = ((GpxDataWrapper) item);
+
+            Float timezoneValue = parseTimezone(tfTimezone.getText().trim());
+            if (timezoneValue == null) {
+                JOptionPane.showMessageDialog(Main.parent, tr("Error while parsing timezone.\nExpected format : {0}", "+H:MM"), 
+                        tr("Invalid timezone"), JOptionPane.ERROR_MESSAGE);
+                continue;
+            }
+            gpstimezone = timezoneValue.floatValue();
+            
+            try {
+                delta = Long.parseLong(tfOffset.getText());
+            } catch(NumberFormatException nfe) {
+                JOptionPane.showMessageDialog(Main.parent, tr("Error while parsing offset.\nExpected format : {0}", "number"), 
+                        tr("Invalid offset"), JOptionPane.ERROR_MESSAGE);
+                continue;
+            }
+            
+            Main.pref.put("tagimages.doublegpstimezone", Double.toString(gpstimezone));
+            Main.pref.put("tagimages.gpstimezone", Long.toString(- ((long) gpstimezone)));
+            Main.pref.put("tagimages.delta", Long.toString(delta * 1000));
+            
+            isOk = true;
+        }
+        
+        // Construct a list of images that have a date, and sort them on the date.
+        ArrayList<ImageEntry> dateImgLst = new ArrayList<ImageEntry>(yLayer.data.size());
+        if (rbAllImg.isSelected()) {
+            for (ImageEntry e : yLayer.data) {
+                if (e.time != null) {
+                    dateImgLst.add(e);
+                }
+            }
+        
+        } else if (rbNoExifImg.isSelected()) {
+            for (ImageEntry e : yLayer.data) {
+                if (e.time != null && e.exifCoor == null) {
+                    dateImgLst.add(e);
+                }
+            }
+
+        } else { // rbUntaggedImg.isSelected()  
+            for (ImageEntry e : yLayer.data) {
+                if (e.time != null && e.coor == null) {
+                    dateImgLst.add(e);
+                }
+            }
+        }
+        
+        int matched = matchGpxTrack(dateImgLst, selectedGpx.data, (long) (gpstimezone * 3600000) + delta * 1000);
+
+        // Search whether an other layer has yet defined some bounding box. 
+        // If none, we'll zoom to the bounding box of the layer with the photos.
+        Collection<Layer> layerCol = Main.map.mapView.getAllLayers();
+        Iterator<Layer> layerIter = layerCol.iterator();
+        boolean boundingBoxedLayerFound = false; 
+        while (layerIter.hasNext()) {
+            Layer l = layerIter.next();
+            if (l != yLayer) {
+                BoundingXYVisitor bbox = new BoundingXYVisitor();
+                l.visitBoundingBox(bbox);
+                if (bbox.min != null && bbox.max != null) {
+                    boundingBoxedLayerFound = true;
+                    break;
+                }
+            }
+        }
+        if (! boundingBoxedLayerFound) {
+            BoundingXYVisitor bbox = new BoundingXYVisitor();
+            yLayer.visitBoundingBox(bbox);
+            Main.map.mapView.recalculateCenterScale(bbox);
+        }
+        
+        Main.main.map.repaint();
+        
+        JOptionPane.showMessageDialog(Main.parent, tr("Found {0} matchs of {1} in GPX track {2}", matched, dateImgLst.size(), selectedGpx.name), 
+                tr("GPX Track loaded"), 
+                ((dateImgLst.size() > 0 && matched == 0) ? JOptionPane.WARNING_MESSAGE 
+                                                         : JOptionPane.INFORMATION_MESSAGE));
+        
+    }
+
+    private int matchGpxTrack(ArrayList<ImageEntry> dateImgLst, GpxData selectedGpx, long offset) {
+        int ret = 0;
+        
+        Collections.sort(dateImgLst, new Comparator<ImageEntry>() {
+            public int compare(ImageEntry arg0, ImageEntry arg1) {
+                return arg0.time.compareTo(arg1.time);
+            }
+        });
+        
+        PrimaryDateParser dateParser = new PrimaryDateParser();
+        
+        for (GpxTrack trk : selectedGpx.tracks) {
+            for (Collection<WayPoint> segment : trk.trackSegs) {
+                
+                long prevDateWp = 0;
+                WayPoint prevWp = null;
+                
+                for (WayPoint curWp : segment) {
+                
+                    String curDateWpStr = (String) curWp.attr.get("time"); 
+                    if (curDateWpStr != null) {
+                        
+                        try {
+                            long curDateWp = dateParser.parse(curDateWpStr).getTime() + offset;
+                            ret += matchPoints(dateImgLst, prevWp, prevDateWp, curWp, curDateWp);
+                            
+                            prevWp = curWp;
+                            prevDateWp = curDateWp;
+
+                        } catch(ParseException e) {
+                            System.err.println("Error while parsing date \"" + curDateWpStr + '"');
+                            e.printStackTrace();
+                            prevWp = null;
+                            prevDateWp = 0;
+                        }
+                    } else {
+                        prevWp = null;
+                        prevDateWp = 0;
+                    }
+                }
+            }
+        }
+        return ret;
+    }
+
+    private int matchPoints(ArrayList<ImageEntry> dateImgLst, WayPoint prevWp, long prevDateWp, WayPoint curWp, long curDateWp) {
+        int ret = 0;
+        int i = getLastIndexOfListBefore(dateImgLst, curDateWp);
+        if (i >= 0 && i < dateImgLst.size() && dateImgLst.get(i).time.getTime() > prevDateWp) {
+            Double speed = null;
+            Double prevElevation = null;
+            Double curElevation = null;
+            if (prevWp != null) {
+                double distance = getDistance(prevWp, curWp);
+                speed = new Double((1000 * distance) / (curDateWp - prevDateWp));
+                try {
+                    prevElevation = new Double((String) prevWp.attr.get("ele"));
+                } catch(Exception e) {
+                }
+            }
+            try {
+                curElevation = new Double((String) curWp.attr.get("ele"));
+            } catch (Exception e) {
+            }
+            
+            while(i >= 0 
+                    && dateImgLst.get(i).time.getTime() == curDateWp) {
+                dateImgLst.get(i).pos = curWp.eastNorth; 
+                dateImgLst.get(i).coor = Main.proj.eastNorth2latlon(dateImgLst.get(i).pos);
+                dateImgLst.get(i).speed = speed;
+                dateImgLst.get(i).elevation = curElevation;
+                ret++;
+                i--;
+            }
+            
+            if (prevDateWp != 0) {
+                long imgDate;
+                while(i >= 0 
+                        && (imgDate = dateImgLst.get(i).time.getTime()) > prevDateWp) {
+                    dateImgLst.get(i).pos = new EastNorth(
+                            prevWp.eastNorth.east() + ((curWp.eastNorth.east() - prevWp.eastNorth.east()) * (imgDate - prevDateWp)) / (curDateWp - prevDateWp),
+                            prevWp.eastNorth.north() + ((curWp.eastNorth.north() - prevWp.eastNorth.north()) * (imgDate - prevDateWp)) / (curDateWp - prevDateWp)); 
+                    dateImgLst.get(i).coor = Main.proj.eastNorth2latlon(dateImgLst.get(i).pos);
+                    dateImgLst.get(i).speed = speed;
+                    if (curElevation != null && prevElevation != null) {
+                        dateImgLst.get(i).elevation = prevElevation + ((curElevation - prevElevation) * (imgDate - prevDateWp)) / (curDateWp - prevDateWp);
+                    } 
+                    ret++;
+                    i--;
+                }
+            }
+        }
+        return ret;
+    }
+
+    private int getLastIndexOfListBefore(ArrayList<ImageEntry> dateImgLst, long searchedDate) {
+        int lstSize = dateImgLst.size();
+        if (lstSize == 0 || searchedDate < dateImgLst.get(0).time.getTime()) {
+            return -1;
+        } else if (searchedDate > dateImgLst.get(lstSize - 1).time.getTime()) {
+            return lstSize;
+        } else if (searchedDate == dateImgLst.get(lstSize - 1).time.getTime()) {
+            return lstSize - 1;
+        } else if (searchedDate == dateImgLst.get(0).time.getTime()) {
+            int curIndex = 0;
+            while (curIndex + 1 < lstSize
+                    && dateImgLst.get(curIndex + 1).time.getTime() == searchedDate) {
+                curIndex++;
+            }
+            return curIndex;
+        }
+        
+        int curIndex = 0;
+        int startIndex=0;
+        int endIndex = lstSize - 1;
+        while (endIndex - startIndex > 1) {
+            curIndex = (endIndex + startIndex) / 2;
+            long curDate = dateImgLst.get(curIndex).time.getTime();
+            if (curDate < searchedDate) {
+                startIndex = curIndex;
+            } else if (curDate > searchedDate) {
+                endIndex = curIndex;
+            } else {
+                // Check that there is no image _after_ that one that have exactly the same date.
+                while (curIndex + 1 < lstSize
+                        && dateImgLst.get(curIndex + 1).time.getTime() == searchedDate) {
+                    curIndex++;
+                }
+                return curIndex;
+            }
+        }
+        return startIndex;
+    }
+
+    private String formatTimezone(double timezone) {
+        StringBuffer ret = new StringBuffer();
+        
+        if (timezone < 0) {
+            ret.append('-');
+            timezone = -timezone;
+        } else {
+            ret.append('+');
+        }
+        ret.append((long) timezone).append(':');
+        int minutes = (int) ((timezone % 1) * 60);
+        if (minutes < 10) {
+            ret.append('0');
+        }
+        ret.append(minutes);
+        
+        return ret.toString();
+    }
+    
+    private Float parseTimezone(String timezone) {
+        char sgnTimezone = '+';
+        String hTimezone = "";
+        String mTimezone = "";
+        int state = 1; // 1=start/sign, 2=hours, 3=minutes.
+        for (int i = 0; i < timezone.length(); i++) {
+            char c = timezone.charAt(i);
+            switch (c) {
+            case ' ' :
+                if (state != 2 || hTimezone.length() != 0) {
+                    return null;
+                }
+                break;
+            case '+' :
+            case '-' : 
+                if (state == 1) {
+                    sgnTimezone = c;
+                    state = 2;
+                } else {
+                    return null;
+                }
+                break;
+            case ':' : 
+            case '.' : 
+                if (state == 2) {
+                    state = 3;
+                } else {
+                    return null;
+                }
+                break;
+            case '0' : case '1' : case '2' : case '3' : case '4' : 
+            case '5' : case '6' : case '7' : case '8' : case '9' :
+                switch(state) {
+                case 1 : 
+                    state = 2;
+                    hTimezone += c;
+                    break;
+                case 2 : 
+                    hTimezone += c;
+                    break;
+                case 3 : 
+                    mTimezone += c;
+                    break;
+                default : 
+                    return null;
+                }
+                break;
+            default : 
+                return null;
+            }
+        }
+        int h = Integer.parseInt(hTimezone);
+        int m = Integer.parseInt(mTimezone);
+        if (h > 12 || m > 59 ) {
+            return null;
+        }
+        return new Float((h + m / 60.0) * (sgnTimezone == '-' ? -1 : 1));
+    }
+
+    /** Return the distance in meters between 2 points 
+     * Formula and earth radius from : http://en.wikipedia.org/wiki/Great-circle_distance */
+    public double getDistance(WayPoint p1, WayPoint p2) {
+        double p1Lat = p1.latlon.lat() * Math.PI / 180;
+        double p1Lon = p1.latlon.lon() * Math.PI / 180; 
+        double p2Lat = p2.latlon.lat() * Math.PI / 180;
+        double p2Lon = p2.latlon.lon() * Math.PI / 180;
+        double ret = Math.atan2(Math.sqrt(Math.pow(Math.cos(p2Lat) * Math.sin(p2Lon - p1Lon), 2) 
+                                          + Math.pow(Math.cos(p1Lat) * Math.sin(p2Lat)
+                                                     - Math.sin(p1Lat) * Math.cos(p2Lat) * Math.cos(p2Lon - p1Lon), 2)), 
+                                Math.sin(p1Lat) * Math.sin(p2Lat) 
+                                + Math.cos(p1Lat) * Math.cos(p2Lat) * Math.cos(p2Lon - p1Lon))
+                     * 6372795; // Earth radius, in meters
+        return ret;
+    }
+}
Index: applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/ImageDisplay.java
===================================================================
--- applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/ImageDisplay.java	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/ImageDisplay.java	(revision 10122)
@@ -0,0 +1,602 @@
+// License: GPL. Copyright 2007 by Christian Gallioz (aka khris78)
+
+package org.openstreetmap.josm.plugins.agpifoj;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.MediaTracker;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.awt.geom.Rectangle2D;
+import java.io.File;
+
+import javax.swing.JComponent;
+
+public class ImageDisplay extends JComponent {
+    
+    /** The file that is currently displayed */
+    private File file = null;
+    
+    /** The image currently displayed */
+    private Image image = null;
+    
+    /** The image currently displayed */
+    private boolean errorLoading = false;
+    
+    /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated 
+     * each time the zoom is modified */
+    private Rectangle visibleRect = null;
+    
+    /** When a selection is done, the rectangle of the selection (in image coordinates) */
+    private Rectangle selectedRect = null;
+
+    /** The tracker to load the images */
+    private MediaTracker tracker = new MediaTracker(this);
+
+    private String osdText = null;
+    
+    /** The thread that reads the images. */
+    private class LoadImageRunnable implements Runnable {
+
+        File file = null;
+        
+        public LoadImageRunnable(File file) {
+            this.file = file;
+        }
+        
+        public void run() {
+            Image img = Toolkit.getDefaultToolkit().createImage(file.getPath());
+            tracker.addImage(img, 1);
+            
+            // Wait for the end of loading
+            while (! tracker.checkID(1, true)) {
+                if (this.file != ImageDisplay.this.file) {
+                    // The file has changed 
+                    tracker.removeImage(img);
+                    return;
+                }
+                try {
+                    Thread.sleep(5);
+                } catch (InterruptedException e) {
+                }
+            }
+
+            boolean error = tracker.isErrorID(1);
+            if (img != null && (img.getWidth(null) == 0 || img.getHeight(null) == 0)) {
+                error = true;
+            }
+            
+            synchronized(ImageDisplay.this) {
+                if (this.file != ImageDisplay.this.file) {
+                    // The file has changed 
+                    tracker.removeImage(img);
+                    return;
+                }
+                ImageDisplay.this.image = img;
+                visibleRect = new Rectangle(0, 0, img.getWidth(null), img.getHeight(null));
+                selectedRect = null;
+                errorLoading = error;
+            }
+            tracker.removeImage(img);
+            ImageDisplay.this.repaint();
+        }
+    }
+    
+    private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
+
+        boolean mouseIsDragging = false;
+        Point mousePointInImg = null;
+        
+        /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor 
+         * at the same place */ 
+        public void mouseWheelMoved(MouseWheelEvent e) {
+            File file;
+            Image image;
+            Rectangle visibleRect;
+            
+            synchronized (ImageDisplay.this) {
+                file = ImageDisplay.this.file;
+                image = ImageDisplay.this.image;
+                visibleRect = ImageDisplay.this.visibleRect;
+            }
+            
+            mouseIsDragging = false;
+            selectedRect = null;
+            
+            if (image == null) {
+                return;
+            }
+
+            // Calculate the mouse cursor position in image coordinates, so that we can center the zoom 
+            // on that mouse position. 
+            if (e.getClickCount() == 1 || mousePointInImg == null) {
+                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
+            }
+            
+            // Applicate the zoom to the visible rectangle in image coordinates
+            if (e.getWheelRotation() > 0) {
+                visibleRect.width = visibleRect.width * 3 / 2;
+                visibleRect.height = visibleRect.height * 3 / 2;
+            } else {
+                visibleRect.width = visibleRect.width * 2 / 3;
+                visibleRect.height = visibleRect.height * 2 / 3;
+            }
+            
+            // Check that the zoom doesn't exceed 2:1
+            if (visibleRect.width < getSize().width / 2) {
+                visibleRect.width = getSize().width / 2;
+            }
+            if (visibleRect.height < getSize().height / 2) {
+                visibleRect.height = getSize().height / 2;
+            }
+            
+            // Set the same ratio for the visible rectangle and the display area
+            int hFact = visibleRect.height * getSize().width;
+            int wFact = visibleRect.width * getSize().height;
+            if (hFact > wFact) {
+                visibleRect.width = hFact / getSize().height;
+            } else {
+                visibleRect.height = wFact / getSize().width;
+            }
+            
+            // The size of the visible rectangle is limited by the image size.
+            checkVisibleRectSize(image, visibleRect);
+            
+            // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
+            Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
+            visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width;
+            visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height;
+            
+            // The position is also limited by the image size
+            checkVisibleRectPos(image, visibleRect);
+            
+            synchronized(ImageDisplay.this) {
+                if (ImageDisplay.this.file == file) {
+                    ImageDisplay.this.visibleRect = visibleRect;
+                }
+            }
+            ImageDisplay.this.repaint();
+        }
+
+        /** Center the display on the point that has been clicked */
+        public void mouseClicked(MouseEvent e) {
+            // Move the center to the clicked point.
+            File file;
+            Image image;
+            Rectangle visibleRect;
+            
+            synchronized (ImageDisplay.this) {
+                file = ImageDisplay.this.file;
+                image = ImageDisplay.this.image;
+                visibleRect = ImageDisplay.this.visibleRect;
+            }
+
+            if (image == null) {
+                return;
+            }
+
+            if (e.getButton() != 1) {
+                return;
+            }
+            
+            // Calculate the translation to set the clicked point the center of the view.
+            Point click = comp2imgCoord(visibleRect, e.getX(), e.getY());
+            Point center = getCenterImgCoord(visibleRect);
+            
+            visibleRect.x += click.x - center.x;
+            visibleRect.y += click.y - center.y;
+            
+            checkVisibleRectPos(image, visibleRect);
+            
+            synchronized(ImageDisplay.this) {
+                if (ImageDisplay.this.file == file) {
+                    ImageDisplay.this.visibleRect = visibleRect;
+                }
+            }
+            ImageDisplay.this.repaint();
+        }
+
+        /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of 
+         * a picture part) */
+        public void mousePressed(MouseEvent e) {
+            if (image == null) {
+                mouseIsDragging = false;
+                selectedRect = null;
+                return;
+            }
+            
+            File file;
+            Image image;
+            Rectangle visibleRect;
+            
+            synchronized (ImageDisplay.this) {
+                file = ImageDisplay.this.file;
+                image = ImageDisplay.this.image;
+                visibleRect = ImageDisplay.this.visibleRect;
+            }
+
+            if (image == null) {
+                return;
+            }
+
+            if (e.getButton() == 1) {
+                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                mouseIsDragging = true;
+                selectedRect = null;
+            } else if (e.getButton() == 3) {
+                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                checkPointInVisibleRect(mousePointInImg, visibleRect);
+                mouseIsDragging = false;
+                selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
+                ImageDisplay.this.repaint();
+            } else {
+                mouseIsDragging = false;
+                selectedRect = null;
+            }
+        }
+
+        public void mouseDragged(MouseEvent e) {
+            if (! mouseIsDragging && selectedRect == null) {
+                return;
+            }
+            
+            File file;
+            Image image;
+            Rectangle visibleRect;
+            
+            synchronized (ImageDisplay.this) {
+                file = ImageDisplay.this.file;
+                image = ImageDisplay.this.image;
+                visibleRect = ImageDisplay.this.visibleRect;
+            }
+
+            if (image == null) {
+                mouseIsDragging = false;
+                selectedRect = null;
+                return;
+            }
+
+            if (mouseIsDragging) {
+                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                visibleRect.x += mousePointInImg.x - p.x;
+                visibleRect.y += mousePointInImg.y - p.y;
+                checkVisibleRectPos(image, visibleRect);
+                synchronized(ImageDisplay.this) {
+                    if (ImageDisplay.this.file == file) {
+                        ImageDisplay.this.visibleRect = visibleRect;
+                    }
+                }
+                ImageDisplay.this.repaint();
+                
+            } else if (selectedRect != null) {
+                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
+                checkPointInVisibleRect(p, visibleRect);
+                Rectangle rect = new Rectangle(
+                        (p.x < mousePointInImg.x ? p.x : mousePointInImg.x),
+                        (p.y < mousePointInImg.y ? p.y : mousePointInImg.y),
+                        (p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x),
+                        (p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y));
+                checkVisibleRectSize(image, rect);
+                checkVisibleRectPos(image, rect);
+                ImageDisplay.this.selectedRect = rect;
+                ImageDisplay.this.repaint();
+            }
+
+        }
+
+        public void mouseReleased(MouseEvent e) {
+            if (! mouseIsDragging && selectedRect == null) {
+                return;
+            }
+            
+            File file;
+            Image image;
+            Rectangle visibleRect;
+            
+            synchronized (ImageDisplay.this) {
+                file = ImageDisplay.this.file;
+                image = ImageDisplay.this.image;
+                visibleRect = ImageDisplay.this.visibleRect;
+            }
+
+            if (image == null) {
+                mouseIsDragging = false;
+                selectedRect = null;
+                return;
+            }
+
+            if (mouseIsDragging) {
+                mouseIsDragging = false;
+                
+            } else if (selectedRect != null) {
+                int oldWidth = selectedRect.width;
+                int oldHeight = selectedRect.height;
+
+                // Check that the zoom doesn't exceed 2:1
+                if (selectedRect.width < getSize().width / 2) {
+                    selectedRect.width = getSize().width / 2;
+                }
+                if (selectedRect.height < getSize().height / 2) {
+                    selectedRect.height = getSize().height / 2;
+                }
+                
+                // Set the same ratio for the visible rectangle and the display area
+                int hFact = selectedRect.height * getSize().width;
+                int wFact = selectedRect.width * getSize().height;
+                if (hFact > wFact) {
+                    selectedRect.width = hFact / getSize().height;
+                } else {
+                    selectedRect.height = wFact / getSize().width;
+                }
+                
+                // Keep the center of the selection
+                if (selectedRect.width != oldWidth) {
+                    selectedRect.x -= (selectedRect.width - oldWidth) / 2;
+                }
+                if (selectedRect.height != oldHeight) {
+                    selectedRect.y -= (selectedRect.height - oldHeight) / 2;
+                }
+                
+                checkVisibleRectSize(image, selectedRect);
+                checkVisibleRectPos(image, selectedRect);
+                
+                synchronized (ImageDisplay.this) {
+                    if (file == ImageDisplay.this.file) {
+                        ImageDisplay.this.visibleRect = selectedRect;
+                    }
+                }                
+                selectedRect = null;
+                ImageDisplay.this.repaint();
+            }
+        }
+
+        public void mouseEntered(MouseEvent e) {
+        }
+
+        public void mouseExited(MouseEvent e) {
+        }
+
+        public void mouseMoved(MouseEvent e) {
+        }
+        
+        private void checkPointInVisibleRect(Point p, Rectangle visibleRect) {
+            if (p.x < visibleRect.x) {
+                p.x = visibleRect.x;
+            }
+            if (p.x > visibleRect.x + visibleRect.width) {
+                p.x = visibleRect.x + visibleRect.width;
+            }
+            if (p.y < visibleRect.y) {
+                p.y = visibleRect.y;
+            }
+            if (p.y > visibleRect.y + visibleRect.height) {
+                p.y = visibleRect.y + visibleRect.height;
+            }
+        }
+    }
+    
+    public ImageDisplay() {
+        ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
+        addMouseListener(mouseListener);
+        addMouseWheelListener(mouseListener);
+        addMouseMotionListener(mouseListener);
+    }
+    
+    public void setImage(File file) {
+        synchronized(this) {
+            this.file = file;
+            image = null;
+            selectedRect = null;
+            errorLoading = false;
+        }
+        repaint();
+        if (file != null) {
+            new Thread(new LoadImageRunnable(file)).start();
+        }
+    }
+
+    public void setOsdText(String text) {
+        this.osdText = text;
+    }
+    
+    public void paintComponent(Graphics g) {
+        Image image;
+        File file;
+        Rectangle visibleRect;
+        boolean errorLoading;
+        
+        synchronized(this) {
+            image = this.image;
+            file = this.file;
+            visibleRect = this.visibleRect;
+            errorLoading = this.errorLoading;
+        }
+        
+        if (file == null) {
+            g.setColor(Color.black);
+            String noImageStr = tr("No image");
+            Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
+            Dimension size = getSize();
+            g.drawString(noImageStr,
+                         (int) ((size.width - noImageSize.getWidth()) / 2),
+                         (int) ((size.height - noImageSize.getHeight()) / 2));
+        } else if (image == null) {
+            g.setColor(Color.black);
+            String loadingStr;
+            if (! errorLoading) {;
+                loadingStr = tr("Loading {0}", file.getName());
+            } else {
+                loadingStr = tr("Error on file {0}", file.getName());
+            }
+            Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
+            Dimension size = getSize();
+            g.drawString(loadingStr,
+                         (int) ((size.width - noImageSize.getWidth()) / 2),
+                         (int) ((size.height - noImageSize.getHeight()) / 2));
+        } else {
+            Rectangle target = calculateDrawImageRectangle(visibleRect);
+            g.drawImage(image, 
+                        target.x, target.y, target.x + target.width, target.y + target.height, 
+                        visibleRect.x, visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height, 
+                        null);
+            if (selectedRect != null) {
+                Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y);
+                Point bottomRight = img2compCoord(visibleRect, 
+                                                  selectedRect.x + selectedRect.width, 
+                                                  selectedRect.y + selectedRect.height);
+                g.setColor(new Color(128, 128, 128, 180));
+                g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
+                g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
+                g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
+                g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
+                g.setColor(Color.black);
+                g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
+            }
+            if (errorLoading) {
+                String loadingStr = tr("Error on file {0}", file.getName());
+                Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
+                Dimension size = getSize();
+                g.drawString(loadingStr,
+                             (int) ((size.width - noImageSize.getWidth()) / 2),
+                             (int) ((size.height - noImageSize.getHeight()) / 2));
+            }
+            if (osdText != null) {
+                FontMetrics metrics = g.getFontMetrics(g.getFont());
+                int ascent = metrics.getAscent();
+                Color bkground = new Color(255, 255, 255, 128);
+                int lastPos = 0;
+                int pos = osdText.indexOf("\n");
+                int x = 3;
+                int y = 3;
+                while (pos > 0) {
+                    String line = osdText.substring(lastPos, pos);
+                    Rectangle2D lineSize = metrics.getStringBounds(line, g);
+                    g.setColor(bkground);
+                    g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
+                    g.setColor(Color.black);
+                    g.drawString(line, x, y + ascent);
+                    y += (int) lineSize.getHeight();
+                    lastPos = pos + 1;
+                    pos = osdText.indexOf("\n", lastPos);
+                }
+                if (lastPos > 0) {
+                    String line = osdText.substring(lastPos);
+                    Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g);
+                    g.setColor(bkground);
+                    g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
+                    g.setColor(Color.black);
+                    g.drawString(line, x, y + ascent);
+                }
+            }
+        }
+    }
+    
+    private final Point img2compCoord(Rectangle visibleRect, int xImg, int yImg) {
+        Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
+        return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
+                         drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
+    }
+
+    private final Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp) {
+        Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
+        return new Point(visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width,
+                         visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height);
+    }
+
+   private final Point getCenterImgCoord(Rectangle visibleRect) {
+        return new Point(visibleRect.x + visibleRect.width / 2,
+                                 visibleRect.y + visibleRect.height / 2);
+    }
+
+    private Rectangle calculateDrawImageRectangle(Rectangle visibleRect) {
+        Dimension size = getSize();
+        int x, y, w, h;
+        x = 0;
+        y = 0; 
+        w = size.width;
+        h = size.height;
+        
+        int wFact = w * visibleRect.height;
+        int hFact = h * visibleRect.width;
+        if (wFact != hFact) {
+            if (wFact > hFact) {
+                w = hFact / visibleRect.height;
+                x = (size.width - w) / 2;
+            } else {
+                h = wFact / visibleRect.width;
+                y = (size.height - h) / 2;
+            }
+        }
+        return new Rectangle(x, y, w, h);
+    }
+
+    public void zoomBestFitOrOne() {
+        File file;
+        Image image;
+        Rectangle visibleRect;
+        
+        synchronized (this) {
+            file = ImageDisplay.this.file;
+            image = ImageDisplay.this.image;
+            visibleRect = ImageDisplay.this.visibleRect;
+        }
+
+        if (image == null) {
+            return;
+        }
+
+        if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
+            // The display is not at best fit. => Zoom to best fit
+            visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
+        
+        } else {
+            // The display is at best fit => zoom to 1:1
+            Point center = getCenterImgCoord(visibleRect);
+            visibleRect = new Rectangle(center.x - getWidth() / 2, center.y - getHeight() / 2, 
+                                        getWidth(), getHeight());
+            checkVisibleRectPos(image, visibleRect);
+        }
+        
+        synchronized(this) {
+            if (file == this.file) {
+                this.visibleRect = visibleRect;
+            }
+        }
+        repaint();
+    }
+
+    private final void checkVisibleRectPos(Image image, Rectangle visibleRect) {
+        if (visibleRect.x < 0) {
+            visibleRect.x = 0;
+        }
+        if (visibleRect.y < 0) {
+            visibleRect.y = 0;
+        }
+        if (visibleRect.x + visibleRect.width > image.getWidth(null)) {
+            visibleRect.x = image.getWidth(null) - visibleRect.width;
+        }
+        if (visibleRect.y + visibleRect.height > image.getHeight(null)) {
+            visibleRect.y = image.getHeight(null) - visibleRect.height;
+        }
+    }
+
+    private void checkVisibleRectSize(Image image, Rectangle visibleRect) {
+        if (visibleRect.width > image.getWidth(null)) {
+            visibleRect.width = image.getWidth(null);
+        }
+        if (visibleRect.height > image.getHeight(null)) {
+            visibleRect.height = image.getHeight(null);
+        }
+    }
+}
Index: applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/ScrollViewport.java
===================================================================
--- applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/ScrollViewport.java	(revision 10122)
+++ applications/editors/josm/plugins/agpifoj/src/org/openstreetmap/josm/plugins/agpifoj/ScrollViewport.java	(revision 10122)
@@ -0,0 +1,198 @@
+// License: GPL. Copyright 2007 by Christian Gallioz (aka khris78)
+
+package org.openstreetmap.josm.plugins.agpifoj;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JViewport;
+import javax.swing.Timer;
+
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/** A viewport with UP and DOWN arrow buttons, so that the user can make the
+ * content scroll.
+ */
+public class ScrollViewport extends JPanel {
+
+    private static final int NO_SCROLL = 0;
+    
+    public static final int UP_DIRECTION = 1;
+    public static final int DOWN_DIRECTION = 2;
+    public static final int LEFT_DIRECTION = 4;
+    public static final int RIGHT_DIRECTION = 8;
+	public static final int VERTICAL_DIRECTION = UP_DIRECTION | DOWN_DIRECTION;
+	public static final int HORIZONTAL_DIRECTION = LEFT_DIRECTION | RIGHT_DIRECTION;
+	public static final int ALL_DIRECTION = HORIZONTAL_DIRECTION | VERTICAL_DIRECTION;
+	
+    
+    private class ScrollViewPortMouseListener extends MouseAdapter {
+        
+        private int direction;
+        
+        public ScrollViewPortMouseListener(int direction) {
+            this.direction = direction;
+        }
+
+        @Override public void mouseExited(MouseEvent arg0) {
+            ScrollViewport.this.scrollDirection = NO_SCROLL;
+            timer.stop();
+        }
+
+        @Override public void mouseReleased(MouseEvent arg0) {
+            ScrollViewport.this.scrollDirection = NO_SCROLL;
+            timer.stop();
+        }
+
+        @Override public void mousePressed(MouseEvent arg0) {
+            ScrollViewport.this.scrollDirection = direction;
+            scroll();
+            timer.restart();
+        }
+
+    }
+    
+    private JViewport vp = new JViewport();
+    private JComponent component = null;
+    
+    private Timer timer = new Timer(200, new ActionListener() {
+        public void actionPerformed(ActionEvent arg0) {
+            ScrollViewport.this.scroll();
+        }
+    });
+    
+    private int scrollDirection = NO_SCROLL;
+    
+    public ScrollViewport(JComponent c, int direction) {
+        this(direction);
+        add(c);
+    }
+    
+    public ScrollViewport(int direction) {
+        setLayout(new BorderLayout());
+
+        JButton button;
+        
+        // UP
+        if ((direction & UP_DIRECTION) > 0) {
+        	button = new JButton();
+        	button.addMouseListener(new ScrollViewPortMouseListener(UP_DIRECTION));
+        	button.setPreferredSize(new Dimension(10,10));
+        	button.setIcon(ImageProvider.get("svpUp"));
+        	add(button, BorderLayout.NORTH);
+        }
+
+        // DOWN
+        if ((direction & DOWN_DIRECTION) > 0) {
+        	button = new JButton();
+        	button.addMouseListener(new ScrollViewPortMouseListener(DOWN_DIRECTION));
+        	button.setPreferredSize(new Dimension(10,10));
+        	button.setIcon(ImageProvider.get("svpDown"));
+        	add(button, BorderLayout.SOUTH);
+        }
+        
+        // LEFT
+        if ((direction & LEFT_DIRECTION) > 0) {
+        	button = new JButton();
+        	button.addMouseListener(new ScrollViewPortMouseListener(LEFT_DIRECTION));
+        	button.setPreferredSize(new Dimension(10,10));
+        	button.setIcon(ImageProvider.get("svpLeft"));
+        	add(button, BorderLayout.WEST);
+        }
+        
+        // RIGHT
+        if ((direction & RIGHT_DIRECTION) > 0) {
+        	button = new JButton();
+        	button.addMouseListener(new ScrollViewPortMouseListener(RIGHT_DIRECTION));
+        	button.setPreferredSize(new Dimension(10,10));
+        	button.setIcon(ImageProvider.get("svpRight"));
+        	add(button, BorderLayout.EAST);
+        }
+        
+        add(vp, BorderLayout.CENTER);
+        
+        timer.setRepeats(true);
+        timer.setInitialDelay(400);
+    }
+    
+    public synchronized void scroll() {
+        int direction = scrollDirection;
+        
+        if (component == null || direction == NO_SCROLL) {
+            return;
+        }
+        
+        Dimension compSize = component.getSize();
+        Rectangle viewRect = vp.getViewRect();
+        
+        int delta;
+        int newY = 0;
+        int newX = 0;
+
+        if (direction < LEFT_DIRECTION) {
+        	newX = viewRect.x;
+        	delta = viewRect.height * 4 / 5;
+        } else {
+        	newY = viewRect.y;
+        	delta = viewRect.width * 4 / 5;
+        }
+        
+        switch (direction) {
+        case UP_DIRECTION :
+            newY = viewRect.y - delta;
+            if (newY < 0) {
+                newY = 0;
+            }
+            break;
+        case DOWN_DIRECTION :
+            newY = viewRect.y + delta;
+            if (newY > compSize.height - viewRect.height) {
+                newY = compSize.height - viewRect.height;
+            }
+            break;
+        case LEFT_DIRECTION :
+            newX = viewRect.x - delta;
+            if (newX < 0) {
+                newX = 0;
+            }
+            break;
+        case RIGHT_DIRECTION :
+            newX = viewRect.x + delta;
+            if (newX > compSize.width - viewRect.width) {
+                newX = compSize.width - viewRect.width;
+            }
+            break;
+        default : 
+            throw new IllegalStateException("Unknown direction : " + direction);
+        }
+        
+        vp.setViewPosition(new Point(newX, newY));
+    }
+    
+    public Rectangle getViewRect() {
+    	return vp.getViewRect();
+    }
+
+    public Dimension getViewSize() {
+    	return vp.getViewSize();
+    }
+
+    public Point getViewPosition() {
+    	return vp.getViewPosition();
+    }
+
+    public void add(JComponent c) {
+        vp.removeAll();
+        this.component = c;
+        vp.add(c);
+    }
+}
Index: applications/editors/josm/plugins/build.xml
===================================================================
--- applications/editors/josm/plugins/build.xml	(revision 10121)
+++ applications/editors/josm/plugins/build.xml	(revision 10122)
@@ -14,4 +14,5 @@
   <target name="dist" depends="compile">
     <mkdir dir="../dist"/>
+    <ant	dir="agpifoj"		antfile="build.xml"	target="dist"/>
     <ant	dir="colorscheme"	antfile="build.xml"	target="dist"/>
     <ant	dir="duplicateway"	antfile="build.xml"	target="dist"/>
@@ -41,4 +42,5 @@
 
   <target name="clean">
+    <ant	dir="agpifoj"		antfile="build.xml"	target="clean"/>
     <ant	dir="colorscheme"	antfile="build.xml"	target="clean"/>
     <ant	dir="duplicateway"	antfile="build.xml"	target="clean"/>
Index: applications/editors/josm/plugins/validator/src/org/openstreetmap/josm/plugins/validator/OSMValidatorPlugin.java
===================================================================
--- applications/editors/josm/plugins/validator/src/org/openstreetmap/josm/plugins/validator/OSMValidatorPlugin.java	(revision 10121)
+++ applications/editors/josm/plugins/validator/src/org/openstreetmap/josm/plugins/validator/OSMValidatorPlugin.java	(revision 10122)
@@ -65,5 +65,4 @@
 	public OSMValidatorPlugin()
 	{
-		PreferenceEditor.importOldPreferences();
 		initializeTests( getTests() );
 	}
@@ -82,5 +81,6 @@
 			validationDialog = new ValidatorDialog(this);
 			newFrame.addToggleDialog(validationDialog);
-			Main.main.addLayer(new ErrorLayer(this));
+			if(Main.pref.getBoolean(PreferenceEditor.PREF_LAYER, true))
+				Main.main.addLayer(new ErrorLayer(this));
 			if( Main.pref.hasKey(PreferenceEditor.PREF_DEBUG + ".grid") )
 				Main.main.addLayer(new GridLayer(tr("Grid")));
Index: applications/editors/josm/plugins/validator/src/org/openstreetmap/josm/plugins/validator/PreferenceEditor.java
===================================================================
--- applications/editors/josm/plugins/validator/src/org/openstreetmap/josm/plugins/validator/PreferenceEditor.java	(revision 10121)
+++ applications/editors/josm/plugins/validator/src/org/openstreetmap/josm/plugins/validator/PreferenceEditor.java	(revision 10122)
@@ -32,4 +32,7 @@
 	public static final String PREF_DEBUG = PREFIX + ".debug";
 
+	/** The preferences key for debug preferences */
+	public static final String PREF_LAYER = PREFIX + ".layer";
+
 	/** The preferences key for enabled tests */
 	public static final String PREF_TESTS = PREFIX + ".tests";
@@ -42,4 +45,5 @@
 
 	private JCheckBox prefUseIgnore;
+	private JCheckBox prefUseLayer;
 
 	/** The list of all tests */
@@ -58,4 +62,8 @@
 		prefUseIgnore.setToolTipText(tr("Use the use ignore list to suppress warnings."));
 		testPanel.add(prefUseIgnore, GBC.eol());
+
+		prefUseLayer = new JCheckBox(tr("Use error layer."), Main.pref.getBoolean(PREF_LAYER, true));
+		prefUseLayer.setToolTipText(tr("Use the error layer to display problematic elements."));
+		testPanel.add(prefUseLayer, GBC.eol());
 
 		GBC a = GBC.eol().insets(-5,0,0,0);
@@ -103,37 +111,5 @@
 		Main.pref.put( PREF_TESTS_BEFORE_UPLOAD, testsBeforeUpload.toString());
 		Main.pref.put( PREF_USE_IGNORE, prefUseIgnore.isSelected());
+		Main.pref.put( PREF_LAYER, prefUseLayer.isSelected());
 	}
-	
-	/**
-	 * Import old stored preferences
-	 */
-	public static void importOldPreferences()
-	{
-		if( !Main.pref.hasKey("tests") || !Pattern.matches("(\\w+=(true|false),?)*", Main.pref.get("tests")) )
-			return;
-		
-		String enabledTests = Main.pref.get("tests");
-		Main.pref.put(PREF_TESTS, enabledTests);
-		Main.pref.put("tests", null );
-		
-		StringBuilder testsBeforeUpload = new StringBuilder();
-		Map<String, String> oldPrefs = Main.pref.getAllPrefix("tests");
-		for( Map.Entry<String, String> pref : oldPrefs.entrySet() )
-		{
-			String key = pref.getKey();
-			String value = pref.getValue();
-			if( key.endsWith(".checkBeforeUpload") )
-			{
-				String testName = key.substring(6, key.length() - 18);
-				testsBeforeUpload.append( ',' ).append( testName ).append( '=' ).append( value );
-			}
-			else
-				Main.pref.put( PREFIX + key.substring(5), value );
-			Main.pref.put(key, null );
-		}
-		
-		if (testsBeforeUpload.length() > 0 ) testsBeforeUpload = testsBeforeUpload.deleteCharAt(0);
-		Main.pref.put( PREF_TESTS_BEFORE_UPLOAD, testsBeforeUpload.toString());
-	}
-
 }
