Index: /applications/editors/josm/plugins/contourmerge/LICENSE
===================================================================
--- /applications/editors/josm/plugins/contourmerge/LICENSE	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/LICENSE	(revision 24969)
@@ -0,0 +1,345 @@
+		    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/contourmerge/README
===================================================================
--- /applications/editors/josm/plugins/contourmerge/README	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/README	(revision 24969)
@@ -0,0 +1,22 @@
+README 
+======
+
+The 'contourmerge' plugin provides an editing mode to merge (or align) two contours on an OSM map, i.e.
+ - a section of a lake outline and an a section of neighboring area representing a forest
+ - a section of a coast line and a section of a residential area
+ - two border sections
+ etc.  
+      
+AUTHOR
+======
+Karl Guggisberg <karl.guggisberg@guggis.ch>      
+      
+     
+ 
+  
+
+
+ 
+     
+
+ 
Index: /applications/editors/josm/plugins/contourmerge/build.xml
===================================================================
--- /applications/editors/josm/plugins/contourmerge/build.xml	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/build.xml	(revision 24969)
@@ -0,0 +1,336 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+** This is the build file for the contour plugin
+**
+** Maintaining versions
+** ====================
+** see README.template
+**
+** Usage
+** =====
+** To build it run
+**
+**    > ant  dist
+**
+** To install the generated plugin locally (in your default plugin directory) run
+**
+**    > ant  install
+**
+** To build against the core in ../../core, create a correct manifest and deploy to
+** SVN, 
+**    set the properties commit.message and plugin.main.version
+** and run
+**    > ant  publish
+**
+**
+-->
+<project name="contourmerge" default="dist" basedir=".">
+
+	<property name="commit.message" value="Updating to JOSM 3210" />
+	<property name="plugin.main.version" value="3210" />
+
+	<!--
+      ************************************************
+      ** should not be necessary to change the following properties
+     -->
+	<property name="josm" location="../../core/dist/josm-custom.jar" />
+	<property name="plugin.build.dir" value="build" />
+	<property name="plugin.src.dir" value="src" />
+	<!-- this is the directory where the plugin jar is copied to -->
+	<property name="plugin.dist.dir" value="../../dist" />
+	<property name="ant.build.javac.target" value="1.5" />
+	<property name="plugin.dist.dir" value="../../dist" />
+	<property name="plugin.jar" value="${plugin.dist.dir}/${ant.project.name}.jar" />
+
+	<!--
+    **********************************************************
+    ** init - initializes the build
+    **********************************************************
+    -->
+	<target name="init">
+		<mkdir dir="${plugin.build.dir}" />
+	</target>
+
+	<!--
+    **********************************************************
+    ** compile - complies the source tree
+    **********************************************************
+    -->
+	<target name="compile" depends="init">
+		<echo message="compiling sources for  ${plugin.jar} ... " />
+		<javac srcdir="src" classpath="${josm}" debug="true" destdir="${plugin.build.dir}">
+			<compilerarg value="-Xlint:deprecation" />
+			<compilerarg value="-Xlint:unchecked" />
+		</javac>
+	</target>
+
+	<!--
+    **********************************************************
+    ** dist - creates the plugin jar
+    **********************************************************
+    -->
+	<target name="dist" depends="compile,revision">
+		<echo message="creating ${plugin.jar} for version ${version.entry.commit.revision} ... " />
+		<copy todir="${plugin.build.dir}/images">
+			<fileset dir="images">
+				<include name="*.png" />
+			</fileset>
+		</copy>
+		<copy todir="${plugin.build.dir}">
+			<fileset dir=".">
+				<include name="README" />
+				<include name="LICENSE" />
+			</fileset>
+		</copy>
+		<copy todir="${plugin.build.dir}">
+			<fileset dir="${plugin.src.dir}">
+				<include name="**/*.dtd" />
+			</fileset>
+		</copy>
+		<jar destfile="${plugin.jar}" basedir="${plugin.build.dir}">
+			<manifest>
+				<attribute name="Author" value="Karl Guggisberg" />
+				<attribute name="Plugin-Class" value="org.openstreetmap.josm.plugins.contourmerge.ContourMergePlugin" />
+				<attribute name="Plugin-Date" value="${version.entry.commit.date}" />
+				<attribute name="Plugin-Description" value="Merges the contours of two areas" />
+				<attribute name="Plugin-Icon" value="images/mapmode/contourmerge.png" />
+				<attribute name="Plugin-Link" value="http://wiki.openstreetmap.org/index.php/JOSM/Plugins/ContourMerge" />
+				<attribute name="Plugin-Mainversion" value="${plugin.main.version}" />
+				<attribute name="Plugin-Version" value="${version.entry.commit.revision}" />
+			</manifest>
+		</jar>
+	</target>
+
+	<!--
+    **********************************************************
+    ** revision - extracts the current revision number for the
+    **    file build.number and stores it in the XML property
+    **    version.*
+    **********************************************************
+    -->
+	<target name="revision">
+		<!-- extract the SVN revision information  -->
+		<exec append="false" output="REVISION" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="info" />
+			<arg value="--xml" />
+			<arg value="." />
+		</exec>
+		<xmlproperty file="REVISION" prefix="version" keepRoot="false" collapseAttributes="true" />
+		<delete file="REVISION" />
+	</target>
+
+	<!--
+    **********************************************************
+    ** clean - clean up the build environment
+    **********************************************************
+    -->
+	<target name="clean">
+		<delete dir="${plugin.build.dir}" />
+		<delete file="${plugin.jar}" />
+	</target>
+
+	<!--
+    **********************************************************
+    ** install - install the plugin in your local JOSM installation
+    **********************************************************
+    -->
+	<target name="install" depends="dist">
+		<property environment="env" />
+		<condition property="josm.plugins.dir" value="${env.APPDATA}/JOSM/plugins" else="${user.home}/.josm/plugins">
+			<and>
+				<os family="windows" />
+			</and>
+		</condition>
+		<copy file="${plugin.jar}" todir="${josm.plugins.dir}" />
+	</target>
+
+	<!--
+	 ************************** Publishing the plugin *********************************** 
+	-->
+	<!--
+	** extracts the JOSM release for the JOSM version in ../core and saves it in the 
+	** property ${coreversion.info.entry.revision}
+	**
+	-->
+	<target name="core-info">
+		<exec append="false" output="core.info.xml" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="info" />
+			<arg value="--xml" />
+			<arg value="../../core" />
+		</exec>
+		<xmlproperty file="core.info.xml" prefix="coreversion" keepRoot="true" collapseAttributes="true" />
+		<echo>Building against core revision ${coreversion.info.entry.revision}.</echo>
+		<echo>Plugin-Mainversion is set to ${plugin.main.version}.</echo>
+		<delete file="core.info.xml" />
+	</target>
+
+	<!--
+	** commits the source tree for this plugin
+	-->
+	<target name="commit-current">
+		<echo>Commiting the plugin source with message '${commit.message}' ...</echo>
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="commit" />
+			<arg value="-m '${commit.message}'" />
+			<arg value="." />
+		</exec>
+	</target>
+
+	<!--
+	** updates (svn up) the source tree for this plugin
+	-->
+	<target name="update-current">
+		<echo>Updating plugin source ...</echo>
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="up" />
+			<arg value="." />
+		</exec>
+		<echo>Updating ${plugin.jar} ...</echo>
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="up" />
+			<arg value="../dist/${plugin.jar}" />
+		</exec>
+	</target>
+
+	<!--
+	** commits the plugin.jar 
+	-->
+	<target name="commit-dist">
+		<echo>
+***** Properties of published ${plugin.jar} *****
+Commit message    : '${commit.message}'					
+Plugin-Mainversion: ${plugin.main.version}
+JOSM build version: ${coreversion.info.entry.revision}
+Plugin-Version    : ${version.entry.commit.revision}
+***** / Properties of published ${plugin.jar} *****					
+					
+Now commiting ${plugin.jar} ...
+</echo>
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="-m '${commit.message}'" />
+			<arg value="commit" />
+			<arg value="${plugin.jar}" />
+		</exec>
+	</target>
+
+	<!-- ** make sure svn is present as a command line tool ** -->
+	<target name="ensure-svn-present">
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false" failonerror="false" resultproperty="svn.exit.code">
+			<env key="LANG" value="C" />
+			<arg value="--version" />
+		</exec>
+		<fail message="Fatal: command 'svn --version' failed. Please make sure svn is installed on your system.">
+			<!-- return code not set at all? Most likely svn isn't installed -->
+			<condition>
+				<not>
+					<isset property="svn.exit.code" />
+				</not>
+			</condition>
+		</fail>
+		<fail message="Fatal: command 'svn --version' failed. Please make sure a working copy of svn is installed on your system.">
+			<!-- error code from SVN? Most likely svn is not what we are looking on this system -->
+			<condition>
+				<isfailure code="${svn.exit.code}" />
+			</condition>
+		</fail>
+	</target>
+
+	<target name="publish" depends="ensure-svn-present,core-info,commit-current,update-current,clean,dist,commit-dist">
+	</target>
+
+	<!-- ************************************************************************************ -->
+	<!-- * Targets for compiling and running tests                                            -->
+	<!-- ************************************************************************************ -->
+	<property name="eclipse.plugin.dir" value="C:\software\eclipse-3.6.1\plugins" />
+	<property name="test.build.dir" value="test/build" />
+
+	<path id="groovy.path">
+		<pathelement location="${eclipse.plugin.dir}/org.codehaus.groovy_1.7.5.xx-20100926-2000-e36-RC1\lib\groovy-all-1.7.5.jar" />
+	</path>
+
+	<path id="junit.path">
+		<pathelement location="${eclipse.plugin.dir}/org.junit_4.8.1.v4_8_1_v20100427-1100\junit.jar" />
+	</path>
+
+	<!-- groovy dependency: groovy fails unless hamcrest is on the path -->
+	<path id="hamcrest.path">
+		<pathelement location="test/lib/hamcrest-all-1.3.0RC2.jar" />
+	</path>
+
+	<path id="test.class.path">
+		<pathelement location="${josm}" />
+		<pathelement location="${plugin.build.dir}" />
+		<path refid="groovy.path" />
+		<path refid="junit.path" />
+	</path>
+
+	<path id="groovyc.path">
+		<path refid="junit.path" />
+		<path refid="groovy.path" />
+		<path refid="hamcrest.path" />
+		<pathelement location="${josm}" />
+		<pathelement location="${test.build.dir}" />
+		<pathelement location="${plugin.build.dir}" />
+		<!-- if we didn't explicitly put hamcrest on the class path, groovyc would
+			     abort and report it is missing a hamcrest class -->
+		<pathelement location="test/lib/hamcrest-all-1.2.jar" />
+	</path>
+
+	<target name="test-clean">
+		<delete dir="${test.build.dir}" />
+		<mkdir dir="${test.build.dir}" />
+	</target>
+
+	<target name="test-compile" depends="compile,test-clean" description="Compiles the test files">
+
+		<available classname="org.codehaus.groovy.ant.Groovy" classpathref="groovyc.path" property="groovy.present" />
+		<fail message="Groovy not found. Make sure groovy is on the classpath. Check 'groovy.path' in this build file." unless="groovy.present" />
+
+		<taskdef name="groovyc" classname="org.codehaus.groovy.ant.Groovyc" classpathref="groovy.path" />
+
+		<echo message="compiling test infrastructur for ${plugin.jar} ... " />
+		<javac srcdir="test/src" classpathref="test.class.path" debug="true" destdir="${test.build.dir}" includes="org/openstreetmap/josm/plugins/contourmerge/fixtures/**/*">
+			<compilerarg value="-Xlint:deprecation" />
+			<compilerarg value="-Xlint:unchecked" />
+		</javac>
+
+		<echo message="compiling groovy test cases for ${plugin.jar} ... " />
+		<groovyc srcdir="test/src" destdir="${test.build.dir}" classpathref="groovyc.path">
+		</groovyc>
+
+		<echo message="compiling java test cases for ${plugin.jar} ... " />
+		<javac srcdir="test/src" classpathref="test.class.path" debug="true" destdir="${test.build.dir}">
+			<compilerarg value="-Xlint:deprecation" />
+			<compilerarg value="-Xlint:unchecked" />
+		</javac>
+	</target>
+
+	<target name="test-run" depends="test-compile" description="Runs the junit tests">
+		<delete dir="test/output" />
+		<mkdir dir="test/output" />
+
+		<junit printsummary="true" failureproperty="junit.failure">
+			<classpath>
+				<path refid="groovyc.path" />
+				<pathelement location="test/config" />
+				<!-- required for test config file -->
+				<pathelement location="." />
+				<!-- required to load images from subdir 'images/' -->
+			</classpath>
+
+			<test todir="test/output" name='org.openstreetmap.josm.plugins.contourmerge.AllUnitTests'>
+				<formatter type="xml" />
+			</test>
+		</junit>
+	</target>
+
+	<target name="dev-install" depends="dist">
+		<copy file="${plugin.jar}" todir="C:/data/projekte/osm/josm-dev/plugins" />
+	</target>
+</project>
Index: /applications/editors/josm/plugins/contourmerge/images/mergecontour.svg
===================================================================
--- /applications/editors/josm/plugins/contourmerge/images/mergecontour.svg	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/images/mergecontour.svg	(revision 24969)
@@ -0,0 +1,274 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="24"
+   height="24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.47 r22583"
+   inkscape:export-filename="C:\Users\karl\Desktop\bitmap.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90"
+   sodipodi:docname="aligncontour.svg">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+    <inkscape:perspective
+       id="perspective3598"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3620"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3642"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3664"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3688"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3710"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3732"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3754"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3780"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3802"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3824"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3824-3"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3824-5"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       id="perspective3824-0"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="-91.18248"
+     inkscape:cy="-6.7129775"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1408"
+     inkscape:window-height="813"
+     inkscape:window-x="100"
+     inkscape:window-y="100"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1028.3622)">
+    <path
+       style="fill:#d3defa;stroke:#2961e7;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1"
+       d="m -2.2321429,1.2321428 11.2500001,2.9464286 -4.4642858,8.3035716 4.3750001,7.321428 -11.1607144,9.464286 0,-28.0357142 z"
+       id="path2816"
+       transform="translate(0,1028.3622)" />
+    <path
+       style="fill:#c4edd9;fill-opacity:1;stroke:#0a683a;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 15.267857,1031.5586 13.125,-2.4107 0.08929,26.3393 -13.125,-4.9107 -0.08929,-19.0179 z"
+       id="path2816-1"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="fill:#fadbd3;fill-opacity:1;stroke:#c42916;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 15.267854,1031.5142 0.08929,19.0625 -0.08929,-19.0625 z"
+       id="path2816-1-7"
+       sodipodi:nodetypes="ccc" />
+    <path
+       style="fill:#fadbd3;fill-opacity:1;stroke:#c42916;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 8.983593,1032.6032 -4.3973172,8.192 4.3973172,-8.192 z"
+       id="path2816-1-7-4"
+       sodipodi:nodetypes="ccc" />
+    <path
+       style="fill:#fadbd3;fill-opacity:1;stroke:#c42916;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 4.5647297,1040.9002 4.3526831,7.2098 -4.3526831,-7.2098 z"
+       id="path2816-1-7-4-0"
+       sodipodi:nodetypes="ccc" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#575252;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-0"
+       width="3.5327461"
+       height="3.324851"
+       x="7.8616414"
+       y="1031.4122" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#c42916;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678"
+       width="3.5327461"
+       height="3.324851"
+       x="7.0309782"
+       y="1031.0264" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 9.8174861,1040.3086 4.6914429,-0.1339 0,0"
+       id="path3768"
+       sodipodi:nodetypes="ccc" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 8.8392874,1040.1747 2.1875026,-1.875"
+       id="path3770" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 8.8169654,1040.1747 2.0535736,1.8304"
+       id="path3770-2"
+       sodipodi:nodetypes="cc" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#575252;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-0-7"
+       width="3.5327461"
+       height="3.324851"
+       x="3.9206929"
+       y="1039.276" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#c42916;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-6"
+       width="3.5327461"
+       height="3.324851"
+       x="3.0900292"
+       y="1038.8901" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#575252;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-0-6"
+       width="3.5327461"
+       height="3.324851"
+       x="7.4246588"
+       y="1047.3572" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#c42916;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-1"
+       width="3.5327461"
+       height="3.324851"
+       x="6.5939956"
+       y="1046.9713" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#575252;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-0-4"
+       width="3.5327461"
+       height="3.324851"
+       x="14.022218"
+       y="1049.4722" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#c42916;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-2"
+       width="3.5327461"
+       height="3.324851"
+       x="13.191555"
+       y="1049.0863" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#575252;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-0-9"
+       width="3.5327461"
+       height="3.324851"
+       x="14.274756"
+       y="1030.5635" />
+    <rect
+       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#c42916;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect3678-7"
+       width="3.5327461"
+       height="3.324851"
+       x="13.444093"
+       y="1030.1776" />
+  </g>
+</svg>
Index: /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeMode.java
===================================================================
--- /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeMode.java	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeMode.java	(revision 24969)
@@ -0,0 +1,265 @@
+package org.openstreetmap.josm.plugins.contourmerge;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Cursor;
+import java.awt.Point;
+import java.awt.dnd.DragSource;
+import java.awt.event.MouseEvent;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.BBox;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.help.HelpUtil;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * <p>ContourMergeMode</p> is the {@link MapMode} for mergin the contours of two areas.</p>
+ *
+ */
+public class ContourMergeMode extends MapMode {
+	static private final Logger logger = Logger.getLogger(ContourMergeMode.class.getName());
+
+	private Collection<OsmPrimitive> selection;
+	
+	public ContourMergeMode(MapFrame mapFrame) {
+		super(
+				tr("Contour Merge"),  // name
+				"contourmerge",       // icon name 
+				tr("Merge the contour of an area with the contour of another area"), // tooltip 
+				mapFrame,
+				Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+		);
+		putValue("help", HelpUtil.ht("Plugin/contourmerge"));
+	}
+	
+	protected MapView getMapView(){
+		return Main.map.mapView;
+	}
+
+	@Override
+	public void enterMode() {
+		super.enterMode();
+        getMapView().addMouseListener(this);
+        getMapView().addMouseMotionListener(this);
+        ContourMergePlugin.setEnabled(true);
+        ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+        if (model != null) {
+        	model.reset();
+        }
+        /*
+         * Remind the current selection and clear it; otherwise the rendered selection
+         * might interfer with our unterstanding of "selected" nodes and way slices in
+         * this map mode.
+         */
+        selection = model.getLayer().data.getSelected();
+        model.getLayer().data.clearSelection();
+	}
+
+	@Override
+	public void exitMode() {
+		super.exitMode();
+		getMapView().removeMouseListener(this);
+        getMapView().removeMouseMotionListener(this);
+		ContourMergePlugin.setEnabled(false);
+	    ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+	    if (model != null) {
+        	model.reset();
+    	    /*
+    	     * Restore the last selection.
+    	     */    	 
+        	model.getLayer().data.setSelected(selection);
+     	    selection = null;
+        }
+	}
+
+	@Override
+	public boolean layerIsSupported(Layer l) {
+		return l instanceof OsmDataLayer;
+	}
+
+	@Override
+	public void mouseReleased(MouseEvent e) {
+		if (! ContourMergePlugin.isEnabled()) return;
+		onDrop(e.getPoint());		
+	}
+
+	@Override
+	public void mousePressed(MouseEvent e) {
+		if (! ContourMergePlugin.isEnabled()) return;
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		if (model == null) return;
+		onStartDrag(e.getPoint());
+	}
+	
+	@Override
+	public void mouseClicked(MouseEvent e) {		
+		if (! ContourMergePlugin.isEnabled()) return;
+		if (e.getButton() != MouseEvent.BUTTON1) return;		
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();	
+		if (model == null) return;
+		
+		List<Node> candidates = getMapView().getNearestNodes(e.getPoint(), OsmPrimitive.isSelectablePredicate);
+		if (!candidates.isEmpty()){
+			if (!OsmPrimitive.getFilteredList(candidates.get(0).getReferrers(), Way.class).isEmpty()) {					
+				/*
+				 * clicked on a node which isn't isolated ? => toggle its selected state
+				 */
+				model.toggleSelected(candidates.get(0));
+			}
+		}
+		Main.map.mapView.repaint();
+	}
+
+	protected BBox buildSnapBBox(Point p){
+		MapView mv = Main.map.mapView;
+		LatLon ll = mv.getLatLon(p.x -3, p.y - 3);
+		LatLon ur = mv.getLatLon(p.x + 3, p.y + 3);
+		return new BBox(ll, ur);
+	}
+	
+	protected void showHelpText(String text){
+		Main.map.statusLine.setHelpText(text);
+	}
+
+	@Override
+	public void mouseMoved(MouseEvent e) {
+		if (! ContourMergePlugin.isEnabled()) return;
+		if (ContourMergePlugin.getModelManager().getActiveModel() == null) return;
+		if (e.getButton() != MouseEvent.NOBUTTON) return;
+		List<Node> candidates = getMapView().getNearestNodes(e.getPoint(), OsmPrimitive.isSelectablePredicate);
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		showHelpText("");
+		if (candidates.isEmpty()){
+			model.setFeedbackNode(null);
+			WaySegment ws = getMapView().getNearestWaySegment(e.getPoint(),OsmPrimitive.isSelectablePredicate);
+			if (ws == null){
+				getMapView().setCursor(Cursor.getDefaultCursor());			
+				model.setDragStartFeedbackWaySegment(null);				
+			} else {
+				showHelpText(tr("Drag/drop: drag the way segment an drop it on a target segment"));
+				getMapView().setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));	
+				model.setDragStartFeedbackWaySegment(ws);
+			}
+		} else {
+			if (model.isSelected(candidates.get(0))) {
+				showHelpText(tr("Left-Click: deselect node"));
+				getMapView().setCursor(ImageProvider.getCursor("normal","deselect_node"));
+			} else {
+				if (OsmPrimitive.getFilteredList(candidates.get(0).getReferrers(), Way.class).isEmpty()) {
+					showHelpText(tr("Can''t select an isolated node"));
+					getMapView().setCursor(DragSource.DefaultMoveNoDrop);
+				} else {
+					showHelpText(tr("Left-Click: select node"));
+					getMapView().setCursor(ImageProvider.getCursor("normal","select_node"));
+				}
+			}			
+			model.setFeedbackNode(candidates.get(0));			
+		}
+		Main.map.mapView.repaint();
+	}
+	
+	@Override
+	public void mouseEntered(MouseEvent e) {/* ignore */}
+	@Override
+	public void mouseExited(MouseEvent e) {/* ignore */}
+	
+	@Override
+	public void mouseDragged(MouseEvent e) {
+		onStepDrag(e.getPoint());
+	}
+	
+	/* -------------------------------------------------------------------------------------- */
+	/* drag and drop                                                                          */
+	/* -------------------------------------------------------------------------------------- */
+	protected Point dragStart = null;
+	
+	protected void onStartDrag(Point start) {
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		WaySegment ws = getMapView().getNearestWaySegment(start,OsmPrimitive.isSelectablePredicate);
+		if (ws != null && model.isWaySegmentDragable(ws)) {
+			this.dragStart = start;
+			getMapView().setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+			showHelpText(tr("Drag the way segment and drop it on a target segment"));
+			model.setDragOffset(new Point(0,0));
+			model.setDragStartFeedbackWaySegment(ws);
+			model.setDropFeedbackSegment(null);
+		}
+	}
+	
+	protected void onStepDrag(Point current){		
+		if (dragStart == null) return;  // drag initiated outside of map view ?
+		WaySegment ws = getMapView().getNearestWaySegment(current,OsmPrimitive.isSelectablePredicate);
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		WaySegment newDropTargetFeedbackSegment;
+		if (ws == null){
+			/*
+			 * mouse pointer isn't close to another way, continue dragging
+			 */
+			getMapView().setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+			showHelpText(tr("Drag the way segment and drop it on a target segment"));
+			newDropTargetFeedbackSegment = null;
+		} else if (! model.isPotentialDropTarget(ws)) {
+			/*
+			 * mouse pointer is close to a way segment which isn't part of
+			 * a potential target way slice
+			 */
+			getMapView().setCursor(DragSource.DefaultLinkNoDrop);
+			showHelpText(tr("Drag the way segment and drop it on a target segment"));
+			newDropTargetFeedbackSegment = null;
+		} else {
+			/*
+			 * mouse pointer is close to a way segment which is part of a potential
+			 * target way slice
+			 */
+			getMapView().setCursor(DragSource.DefaultLinkDrop);
+			showHelpText(tr("Drop to align to the the target segment"));
+			newDropTargetFeedbackSegment = ws;
+		}
+		Point offset = new Point(current.x - dragStart.x, current.y - dragStart.y);
+		model.setDragOffset(offset);
+		model.setDropFeedbackSegment(newDropTargetFeedbackSegment);
+		Main.map.mapView.repaint();
+	}
+		
+	protected void onDrop(Point target){
+		if (dragStart == null) return;  // drag initiated outside of map view ?
+		WaySegment ws = getMapView().getNearestWaySegment(target,OsmPrimitive.isSelectablePredicate);
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		if (model.isPotentialDropTarget(ws)){
+			/*
+			 * Merge the way slice given by the drag source onto the way
+			 * slice given by the drop target.
+			 */
+			getMapView().setCursor(Cursor.getDefaultCursor());
+			Command cmd = model.buildContourAlignCommand();
+			if (cmd != null){
+				Main.main.undoRedo.add(cmd);
+			}
+		}
+		
+		/*
+		 * Reset the drag state
+		 */
+		getMapView().setCursor(Cursor.getDefaultCursor());
+		showHelpText(tr("Left-Click: on node to select/unselect; Drag: drag way slice"));  			
+		this.dragStart = null;
+		model.setDragStartFeedbackWaySegment(null);
+		model.setDropFeedbackSegment(null);
+		model.setDragOffset(null);
+		Main.map.mapView.repaint();
+	}	
+}
Index: /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModel.java
===================================================================
--- /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModel.java	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModel.java	(revision 24969)
@@ -0,0 +1,492 @@
+package org.openstreetmap.josm.plugins.contourmerge;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Point;
+import java.awt.geom.Line2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
+import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
+import org.openstreetmap.josm.data.osm.event.DataSetListener;
+import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
+import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
+import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
+import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
+import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
+import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.plugins.contourmerge.util.Assert;
+
+/**
+ * <strong>ContourMergeModel</strong> keeps the current edit state for a specific edit layer,
+ * if the <tt>contourmerge</tt> map mode is enabled.</p>
+ */
+public class ContourMergeModel implements DataSetListener{
+	static private final Logger logger = Logger.getLogger(ContourMergeModel.class.getName());
+	
+	private OsmDataLayer layer;	
+	private Node feedbackNode;
+	private WaySegment dragStartFeedbackSegment;
+	private WaySegment dropFeedbackSegment;
+	private final ArrayList<Node> selectedNodes = new ArrayList<Node>();
+	private Point dragOffset = null;
+	
+	/**
+	 * <p>Creates a new contour merge model for the layer {@code layer}.</p>
+	 * 
+	 * @param layer the data layer. Must not be null. 
+	 * @throws IllegalArgumentException thrown if {@code layer} is null
+	 */
+	public ContourMergeModel(OsmDataLayer layer) throws IllegalArgumentException {
+		Assert.checkArgNotNull(layer, "layer");
+		this.layer = layer;
+	}
+	
+	/**
+	 * <p>Replies the data layer this model operates on.</p>
+	 * 
+	 * @return the data layer
+	 */
+	public OsmDataLayer getLayer() {
+		return layer;
+	}
+	
+	/**
+	 * <p>Replies the node the mouse is currently hovering over.</p>
+	 * 
+	 * @return the node 
+	 */
+	public Node getFeedbackNode(){
+		return feedbackNode;
+	}
+	
+	/**
+	 * <p>Sets the node the mouse is currently hovering over.</p>
+	 * 
+	 * @param node the node 
+	 */
+	public void setFeedbackNode(Node node){
+		this.feedbackNode = node;
+	}
+		
+	public void reset() {
+		setFeedbackNode(null);
+	}	
+	
+	/* ---------------------------------------------------------------------------------------- */
+	/* selecting nodes and way segments                                                         */
+	/* ---------------------------------------------------------------------------------------- */	
+	/**
+	 * <p>Replies true, if {@code node} is currently selected in the contour merge mode.</p>
+	 * 
+	 * @param node the node. Must not be null. Must be owned by this models layer.
+	 * @return true, if {@code node} is currently selected in the contour merge mode.</p>
+	 */
+	public boolean isSelected(Node node) throws IllegalArgumentException{
+		Assert.checkArgNotNull(node, "node");
+		Assert.checkArg(node.getDataSet() == layer.data, "Node must be owned by this contour merge models layer"); // don't translate
+		return selectedNodes.contains(node);
+	}
+	
+	/**
+	 * <p>Selects the node {@code node}.</p>
+	 * 
+	 * @param node the node. Must not be null. Must be owned by this models layer.
+	 * @throws IllegalArgumentException
+	 */
+	public void selectNode(Node node) throws IllegalArgumentException{
+		Assert.checkArgNotNull(node, "node");
+		Assert.checkArg(node.getDataSet() == layer.data, "Node must be owned by this contour merge models layer"); // don't translate
+		if (!isSelected(node)) selectedNodes.add(node);
+	}
+	
+	/**
+	 * <p>Deselects the node {@code node}.</p>
+	 * 
+	 * @param node the node. Must not be null. Must be owned by this models layer.
+	 * @throws IllegalArgumentException
+	 */
+	public void deselectNode(Node node) throws IllegalArgumentException{
+		Assert.checkArgNotNull(node, "node");
+		Assert.checkArg(node.getDataSet() == layer.data, "Node must be owned by this contour merge models layer"); // don't translate
+		selectedNodes.remove(node);
+	}
+	
+	/**
+	 * <p>Toggles whether the node {@code node} is selected or not.</p>
+	 * 
+	 * @param node the node. Must not be null. Must be owned by this models layer.
+	 * @throws IllegalArgumentException
+	 */
+	public void toggleSelected(Node node) throws IllegalArgumentException {
+		Assert.checkArgNotNull(node, "node");
+		Assert.checkArg(node.getDataSet() == layer.data, "Node must be owned by this contour merge models layer"); // don't translate
+		if (isSelected(node)) {
+			deselectNode(node);
+		} else {
+			selectNode(node);
+		}		
+	}
+	
+	/**
+	 * <p>Deselects all nodes.</p>
+	 */
+	public void deselectAllNodes(){
+		selectedNodes.clear();
+	}
+	
+	/**
+	 * <p>Replies an <strong>unmodifiable</strong> list of the currently selected nodes.</p>
+	 * 
+	 * @return an <strong>unmodifiable</strong> list of the currently selected nodes.</p>
+	 */
+	public List<Node> getSelectedNodes() {
+		return Collections.unmodifiableList(selectedNodes);
+	}
+
+	/**
+	 * <p>Sets the way segment which would be affected by the next drag/drop
+	 * operation.</p>
+	 * 
+	 * @param segment the way segment. null, if there is no feedback way segment 
+	 */
+	public void setDragStartFeedbackWaySegment(WaySegment segment){
+		this.dragStartFeedbackSegment = segment;
+	}
+	
+	/**
+	 * <p>Replies the current feedback way segment or null, if there is currently
+	 * no such segment
+	 * 
+	 * @return the feedback way segment 
+	 */
+	public WaySegment getDragStartFeedbackWaySegement(){
+		return dragStartFeedbackSegment;
+	}
+	
+	public void setDropFeedbackSegment(WaySegment segment){
+		this.dropFeedbackSegment = segment;
+	}
+	
+	public WaySegment getDropFeedbackSegment(){
+		return dropFeedbackSegment;
+	}
+	
+	/**
+	 * <p>Replies the set of selected ways, i.e. the set of all parent ways of the
+	 * selected nodes.</p>
+	 * 
+	 * @return the set of selected ways 
+	 */
+	protected Set<Way> computeSelectedWays(){
+		Set<Way> ways = new HashSet<Way>();
+		for (Node n: selectedNodes){
+			ways.addAll(OsmPrimitive.getFilteredList(n.getReferrers(), Way.class));
+		}
+		return ways;		
+	}
+	
+	/**
+	 * <p>Replies the set of selected nodes on the way {@code way}.</p>
+	 * 
+	 * @param way the way
+	 * @return the set of selected nodes
+	 */
+	protected Set<Node> computeSelectedNodesOnWay(Way way){
+		Set<Node> nodes = new HashSet<Node>();
+		if (way == null) return nodes;
+		for (Node n : selectedNodes){
+			if (!OsmPrimitive.getFilteredSet(n.getReferrers(), Way.class).contains(way)) continue;
+			nodes.add(n);			
+		}
+		return nodes;
+	}
+	
+	/**
+	 * <p>Replies true, if we can start a drag/drop operation on way slice which is
+	 * given by the currently selected nodes and the way segment {@code ws}.</p>
+	 * 
+	 *  @return true, if we can start a drag/drop operation. false, otherwise 
+	 */
+	public boolean isWaySegmentDragable(WaySegment ws){
+		WaySlice slice = getWaySliceFromSelectedNodes(ws);
+		return slice != null;
+	}
+	
+	/**
+	 * <p>Replies true, if {@code ws} is part of a potential drop target.</p>
+	 *  
+	 * @param ws the way segment. If null, replies false. 
+	 * @return  true, if {@code ws} is part of a potential drop target
+	 */
+	public boolean isPotentialDropTarget(WaySegment ws){
+		if (ws == null) return false;
+		WaySlice dropTarget = getWaySliceFromSelectedNodes(ws);
+		if (dropTarget == null) return false;
+		
+		// make sure we don't try to drop on the drag source, not even
+		// on a different way slice on the way we drag from
+		WaySlice dragSource = getDragSource();
+		if (dragSource == null) return true;
+		return ! dragSource.getWay().equals(dropTarget.getWay());
+	}
+	
+	protected List<Integer> computeSelectedNodeIndicesOnWay(Way way){
+		Set<Node> nodes = computeSelectedNodesOnWay(way);
+		List<Integer> ret = new ArrayList<Integer>();
+		if (nodes.isEmpty()) return ret;
+		for (Node n: nodes){
+			ret.add(way.getNodes().indexOf(n));
+		}
+		Collections.sort(ret);
+		return ret;
+	}
+	
+	protected WaySlice getWaySliceFromSelectedNodes(WaySegment referenceSegment){
+		if (referenceSegment == null) return null;
+		Way way = referenceSegment.way;
+		if (way.isClosed()){			
+			/*
+			 * This is a closed way. We need at least two selected nodes to come up
+			 * with a way slice. 
+			 */
+			List<Integer> selIndices = computeSelectedNodeIndicesOnWay(way);
+			if (selIndices.size() <2) return null;
+			
+			int nn= way.getNodesCount();
+			int li = referenceSegment.lowerIndex;
+			int lower = -1; int upper = nn;
+			/*
+			 * Find the first selected node to the "left" of the way segment, wrapping
+			 * around at the join-node, if necessary.
+			 */
+			for (int i=li; i>=0;i--){
+				if (selIndices.contains(i)) {lower = i; break;}
+			}
+			if (lower == -1){ // not found yet - wrap around and continue search
+				for (int i=nn-1; i>li; i--){
+					if (selIndices.contains(i)) {lower = i; break;}
+				}				
+			}
+			/*
+			 * Find the first selected node to the "right" of the way segment, wrapping
+			 * around at the join-node, if necessary.
+			 */
+			for (int i=li+1; i< nn-1 ; i++){
+				if (selIndices.contains(i)) {upper = i; break;}
+			}
+			if (upper == nn){ // not found yet - wrap around and continue search
+				for (int i=0; i<li; i++){
+					if (selIndices.contains(i)) {upper = i; break;}
+				}
+				/*
+				 * not really a wrap around? => adjust the index
+				 */
+				if (upper == 0) upper = nn-1; 
+			}
+			if (lower < upper){
+				if (upper == nn -1) {
+					return new WaySlice(way, 0, lower, false /* reverse direction */);
+				} else {
+					return new WaySlice(way, lower,upper);
+				}
+			} else if (lower == upper ){
+				return new WaySlice(way, 0, upper, false /* reverse direction */);
+			} else {
+				return new WaySlice(way, upper, lower, false /* reverse direction */);
+			}
+		} else {
+			/*
+			 * This is an open way. We can always reply a way slice. If no nodes are selected, 
+			 * we drag the entire way. If 1 node
+			 * is selected, the way segment determines whether we drag the first or the second
+			 * half. If more than 1 nodes are selected, we drag the way slice between two selected, or
+			 * the first or the last node respectively.
+			 */
+			List<Integer> selIndices = computeSelectedNodeIndicesOnWay(referenceSegment.way);		
+			int nn= way.getNodesCount();
+			int li = referenceSegment.lowerIndex;
+			int lastPos = nn -1;
+			int lower = 0; int upper = lastPos;
+			for (int pos=li; pos >=0; pos--){
+				if (selIndices.contains(pos)) {lower = pos; break;}
+			}
+			for (int pos=li+1; pos <=lastPos; pos++){
+				if (selIndices.contains(pos)) {upper = pos; break;}
+			}
+			return new WaySlice(referenceSegment.way, lower, upper);
+		}
+	}
+	
+	/**
+	 * <p>Replies the way slice we are currently dragging, or null, if if we
+	 * aren't in a drag operation.</p>
+	 * 
+	 * @return the way slice or null
+	 */
+	public WaySlice getDragSource(){
+		if (dragStartFeedbackSegment == null) return null;
+		return getWaySliceFromSelectedNodes(dragStartFeedbackSegment);
+	}
+
+	/**
+	 * <p>Replies the way slice we are currently hovering over and which is suitable
+	 * as drop target, or null, if no such way slice is currently known.</p>
+	 * 
+	 * @return the way slice or null
+	 */
+	public WaySlice getDropTarget(){
+		if (dropFeedbackSegment == null) return null;
+		return getWaySliceFromSelectedNodes(dropFeedbackSegment);
+	}
+
+	/**
+	 * <p>Sets the current drag offset, relative to the point where the drag operation
+	 * started. Set null to indicate, that there is currently no drag operation. </p>
+	 * 
+	 * @param offset the drag offset
+	 */
+	public void setDragOffset(Point offset){
+		this.dragOffset = offset;
+	}
+	
+	/**
+	 * <p>Replies the current drag offset or null, if we aren't in a drag operation.</p>
+	 * 
+	 * @return the drag offset 
+	 */
+	public Point getDragOffset(){
+		return dragOffset;
+	}
+	
+	/**
+	 * <p>Replies true, if we are currently in a drag operation.</p>
+	 * 
+	 * @return true, if we are currently in a drag operation
+	 */
+	public boolean isDragging() {
+		return dragOffset != null;
+	}
+	
+	/**
+	 * <p>Builds the command to align the two contours. Replies null, if the command
+	 * can't be created, i.e. because there is no defined drag source or drop target.</p>
+	 * 
+	 * @return the contour align command
+	 */
+	public Command buildContourAlignCommand() {
+		WaySlice dragSource = getDragSource();
+		WaySlice dropTarget = getDropTarget();
+		if (dragSource == null) return null;
+		if (dropTarget == null) return null;
+		List<Node> targetNodes = dropTarget.getNodes();
+		if (! areDirectionAligned(dragSource, dropTarget)) {
+			Collections.reverse(targetNodes);
+		}
+		List<Command> cmds = new ArrayList<Command>();
+		// the command to change the source way
+		cmds.add(new ChangeCommand(dragSource.getWay(), dragSource.replaceNodes(targetNodes)));
+		
+		// the command to delete nodes we don't need anymore 
+		for (Node n: dragSource.getNodes()) {
+			List<OsmPrimitive> parents = n.getReferrers();
+			parents.remove(dragSource.getWay());
+			if (parents.isEmpty() && !n.isTagged()) {
+				cmds.add(new DeleteCommand(n));
+			}
+		}
+		
+		SequenceCommand cmd = new SequenceCommand(tr("Merging Contour"), cmds);
+		return cmd;
+	}
+	
+	/**
+	 * <p>Replies true, if the two polylines given by the node lists {@code n1} and
+	 * {@code n2} are "direction aligned". Their direction is aligned, if the two lines
+	 * between the two start nodes and the two end nodes of {@code n1} and code {@code n2}
+	 * respectively, do not interesect.</p> 
+	 * 
+	 * @param n1 the first list of nodes 
+	 * @param n2 the second list of nodes 
+	 * @return true, if the two polylines are "direction aligned".
+	 */
+	protected boolean areDirectionAligned(List<Node> n1, List<Node> n2) {
+		EastNorth s1 = n1.get(0).getEastNorth();
+		EastNorth s2 = n1.get(n1.size()-1).getEastNorth();
+		
+		EastNorth t1 = n2.get(0).getEastNorth();
+		EastNorth t2 = n2.get(n2.size()-1).getEastNorth();
+
+		Line2D l1 = new Line2D.Double(s1.getX(), s1.getY(), t1.getX(),t1.getY());
+		Line2D l2 = new Line2D.Double(s2.getX(), s2.getY(), t2.getX(),t2.getY());
+		return ! l1.intersectsLine(l2);
+
+	}
+	
+	/**
+	 * <p>Replies true, if the two way slices are "direction aligned".</p>
+	 * 
+	 * @param dragSource the first way slice 
+	 * @param dropTarget the second way slice 
+	 * @return  true, if the two way slices are "direction aligned"
+	 * @see #areDirectionAligned(List, List)
+	 */
+	protected boolean areDirectionAligned(WaySlice dragSource, WaySlice dropTarget){
+		if (dragSource == null) return false;
+		if (dropTarget == null) return false;
+		return areDirectionAligned(dragSource.getWay().getNodes(), dropTarget.getWay().getNodes());
+	}
+
+	protected void ensureSelectedNodesConsistent() {
+		Iterator<Node> it = selectedNodes.iterator();
+		while(it.hasNext()) {
+			Node n = it.next();
+			if (OsmPrimitive.getFilteredSet(n.getReferrers(), Way.class).isEmpty()) {
+				it.remove();
+			} else if (n.isDeleted()) {
+				it.remove();
+			}			 
+		}
+	}
+	
+	/* ------------------------------------------------------------------------------- */
+	/* interface DataSetListener                                                       */
+	/* ------------------------------------------------------------------------------- */
+
+	@Override
+	public void primtivesRemoved(PrimitivesRemovedEvent event) {
+		ensureSelectedNodesConsistent();
+	}
+
+	@Override
+	public void wayNodesChanged(WayNodesChangedEvent event) {
+		ensureSelectedNodesConsistent();
+	}
+
+	@Override
+	public void dataChanged(DataChangedEvent event) {
+		ensureSelectedNodesConsistent();
+	}
+
+	public void relationMembersChanged(RelationMembersChangedEvent event) {/* ignore */}
+	public void otherDatasetChange(AbstractDatasetChangedEvent event) {/*ignore */}
+	public void primtivesAdded(PrimitivesAddedEvent event) {/* ignore */}
+	public void tagsChanged(TagsChangedEvent event) { /* ignore */}
+	public void nodeMoved(NodeMovedEvent event) {/* ignore */}
+}
Index: /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModelManager.java
===================================================================
--- /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModelManager.java	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModelManager.java	(revision 24969)
@@ -0,0 +1,92 @@
+package org.openstreetmap.josm.plugins.contourmerge;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.plugins.contourmerge.util.Assert;
+
+/**
+ * <p>Manages a set of {@link ContourMergeModel}s for each available data layer.</p>
+ * 
+ * <p>Listens to layer change events and creates new contour merge models for newly added layer, or
+ * removes contour merge models, if a layer is deleted.</p>
+ */
+public class ContourMergeModelManager implements LayerChangeListener{
+	
+	static private ContourMergeModelManager instance;
+	static public ContourMergeModelManager getInstance() {
+		if (instance == null){
+			instance = new ContourMergeModelManager();
+		}
+		return instance;
+	}
+	
+	private final Map<OsmDataLayer, ContourMergeModel> models = new HashMap<OsmDataLayer, ContourMergeModel>();
+	
+	public void wireToJOSM(){
+		models.clear();
+		MapView.addLayerChangeListener(this);
+	}
+	
+	public void unwireFromJOSM() {
+		models.clear();
+		MapView.removeLayerChangeListener(this);
+	}
+	
+	/**
+	 * <p>Replies the contour merge model for the data layer {@code layer}, or null, if no
+	 * such model exists.</p>
+	 * 
+	 * @param layer the data layer. Must not be null.
+	 * @return the model
+	 * @throws IllegalArgumentException thrown if {@code layer} is null
+	 */
+	public ContourMergeModel getModel(OsmDataLayer layer) throws IllegalArgumentException{
+		Assert.checkArgNotNull(layer, "layer");
+		return models.get(layer);
+	}
+	
+	/**
+	 * <p>Replies the contour model for the currently active data layer (the "edit layer"), or null,
+	 * if the currently active layer isn't a data layer.</p>
+	 * 
+	 * @return the model
+	 */
+	public ContourMergeModel getActiveModel() {
+		if (Main.map == null) return null;
+		if (Main.map.mapView == null) return null;
+		OsmDataLayer layer = Main.map.mapView.getEditLayer();
+		if (layer == null) return null;
+		return getModel(layer);
+	}
+	
+	
+	/* ----------------------------------------------------------------------------------- */
+	/* interface LayerChangeListener                                                       */
+	/* ----------------------------------------------------------------------------------- */
+	@Override
+	public void activeLayerChange(Layer oldLayer, Layer newLayer) {/* ignore */}
+	@Override
+	
+	public void layerAdded(Layer newLayer) {
+		if (! (newLayer instanceof OsmDataLayer)) return;
+		OsmDataLayer dl = (OsmDataLayer)newLayer;
+		ContourMergeModel model = new  ContourMergeModel(dl);
+		dl.data.addDataSetListener(model);		
+		models.put((OsmDataLayer)newLayer, model);
+	}
+	
+	@Override
+	public void layerRemoved(Layer oldLayer) {
+		if (! (oldLayer instanceof OsmDataLayer)) return;
+		OsmDataLayer dl = (OsmDataLayer)oldLayer;
+		ContourMergeModel model = models.get(dl);
+		dl.data.removeDataSetListener(model);
+		models.remove(dl);		
+	}
+}
Index: /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergePlugin.java
===================================================================
--- /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergePlugin.java	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergePlugin.java	(revision 24969)
@@ -0,0 +1,48 @@
+package org.openstreetmap.josm.plugins.contourmerge;
+
+import org.openstreetmap.josm.gui.IconToggleButton;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginInformation;
+
+/**
+ * <strong>ContourMergePlugin</strong> is the main class for the <tt>contourmerge</tt> 
+ * plugin.
+ */
+public class ContourMergePlugin extends Plugin{
+	private ContourMergeMode mode;
+	
+	public ContourMergePlugin(PluginInformation info) {
+		super(info);
+	}
+
+	@Override
+	public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+		if (newFrame == null){
+			ContourMergeModelManager.getInstance().unwireFromJOSM();
+			ContourMergeView.getInstance().unwireFromJOSM();
+		} else {
+			newFrame.addMapMode(new IconToggleButton(mode = new ContourMergeMode(newFrame)));
+			ContourMergeModelManager.getInstance().wireToJOSM();
+			ContourMergeView.getInstance().wireToJOSM();
+		}
+	}		
+	
+	static private boolean modeEnabled;
+
+	static public ContourMergeModelManager getModelManager() {
+		return ContourMergeModelManager.getInstance();
+	}
+	
+	static public ContourMergeView getView() {
+		return ContourMergeView.getInstance();
+	}
+	
+	static public boolean isEnabled(){
+		return modeEnabled;
+	}
+	
+	static public void setEnabled(boolean enabled){
+		modeEnabled = enabled;
+	}
+}
Index: /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeView.java
===================================================================
--- /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeView.java	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeView.java	(revision 24969)
@@ -0,0 +1,337 @@
+package org.openstreetmap.josm.plugins.contourmerge;
+
+import static org.openstreetmap.josm.plugins.contourmerge.util.Assert.checkArgNotNull;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.util.logging.Logger;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.MapViewPaintable;
+
+/**
+ * <p><strong>ContourMergeView</strong> renders the {@link ContourMergeModel} for the 
+ * currently active data layer.</p>
+ *
+ */
+public class ContourMergeView implements MapViewPaintable{
+	static private final Logger logger = Logger.getLogger(ContourMergeView.class.getName());
+	
+	static private ContourMergeView instance;
+	
+	public static ContourMergeView getInstance() {
+		return instance == null ? instance = new ContourMergeView() : instance;
+	}
+	
+	public void wireToJOSM() {
+		if (Main.map == null) return;
+		if (Main.map.mapView == null) return;
+		Main.map.mapView.addTemporaryLayer(this);
+	}
+	
+	public void unwireFromJOSM() {
+		if (Main.map == null) return;
+		if (Main.map.mapView == null) return;
+		Main.map.mapView.removeTemporaryLayer(this);
+	}
+	
+	protected void decorateFeedbackNode(Graphics2D g, MapView mv, Bounds bbox){
+		Node n = ContourMergePlugin.getModelManager().getActiveModel().getFeedbackNode();
+		if (n == null) return;
+		/* currently no decoration - mouse pointer is chaning if mouse over a node */
+	}
+	
+	protected void decorateSelectedNode(Graphics2D g, MapView mv, Bounds bbox, Node node){
+		if (!bbox.contains(node.getCoor())) return;
+		Point p = mv.getPoint(node.getCoor());
+		g.translate(p.x,p.y);
+			g.setColor(Color.ORANGE);
+			g.setStroke(new BasicStroke(3,BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+			g.drawLine(-5, 5, 5,-5);
+			g.drawLine(-5, -5, 5, 5);
+			g.setColor(Color.ORANGE.brighter());
+			g.setStroke(new BasicStroke(1,BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+			g.drawLine(-5, 5, 5,-5);
+			g.drawLine(-5, -5, 5, 5);
+		g.translate(-p.x, -p.y);
+	}
+	
+	protected void decorateSelectedNodes(Graphics2D g, MapView mv, Bounds bbox){
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		if (model == null) return;
+		for (Node n: model.getSelectedNodes()) {
+			decorateSelectedNode(g, mv, bbox, n);
+		}
+	}
+	
+	/**
+	 * <p>Highlights a way slice, i.e. the current drag source or a potential drop
+	 * target.</p>
+	 * 
+	 * @param g graphics context
+	 * @param mv map view 
+	 * @param bbox map bbox 
+	 * @param slice the way slice. Must not be null.
+	 */
+	protected void highlightWaySlice(Graphics2D g, MapView mv, Bounds bbox, WaySlice slice){
+		Path2D polyline = project(mv, slice);
+		g.setColor(Color.RED);
+		g.setStroke(new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
+		g.draw(polyline);		
+	}
+	
+	/**
+	 * <p>Projects this way slice onto the map view {@code mv}. Replies a polyline representing
+	 * the way slice on screen.</p>
+	 * 
+	 * @param mv the map view. Must not be null.
+	 * @param ws the way slice. Must not be null.
+	 * @return a polyline 
+	 * @throws IllegalArgumentException thrown if {@code mv} is null
+	 * @throws IllegalArgumentException thrown if {@code ws} is null
+	 * 
+	 */
+	public static Path2D project(MapView mv, WaySlice ws) throws IllegalArgumentException{
+		checkArgNotNull(mv, "mv");
+		checkArgNotNull(ws, "ws");
+		Path2D.Float polyline = new Path2D.Float();
+		if (ws.isInDirection()) {
+			/*
+			 * a way slice of an open way, or a closed way between two nodes in the
+			 * ways direction
+			 */
+			for (int i = ws.getStart(); i <= ws.getEnd(); i++){
+				Point p = mv.getPoint(ws.getWay().getNode(i).getCoor());
+				if (i == ws.getStart()) {
+					polyline.moveTo(p.x, p.y);
+				} else {
+					polyline.lineTo(p.x, p.y);
+				}
+			}
+		} else {
+			/*
+			 * a way slice of a closed way, between two nodes in the direction *opposite*
+			 * to the ways direction
+			 */
+			for (int i = ws.getStart(); i >= 0; i--){
+				Point p = mv.getPoint(ws.getWay().getNode(i).getCoor());
+				if (i == ws.getStart()) {
+					polyline.moveTo(p.x, p.y);
+				} else {
+					polyline.lineTo(p.x, p.y);
+				}
+			}
+			for (int i = ws.getWay().getNodesCount()-2; i >= ws.getEnd(); i--){
+				Point p = mv.getPoint(ws.getWay().getNode(i).getCoor());
+				if (i == ws.getStart()) {
+					polyline.moveTo(p.x, p.y);
+				} else {
+					polyline.lineTo(p.x, p.y);
+				}
+			}
+		}
+		return polyline;
+	}
+	
+	/**
+	 * <p>Projects this way slice onto the map view {@code mv}. Replies a polyline representing
+	 * the way slice on screen, displaced by the offset {@code displacement.x} in x-direction
+	 * and {@code displacement.y} in y-direction. </p>
+	 * 
+	 * @param mv the map view. Must not be null.
+	 * @param ws the way slice. Must not be null.
+	 * @param displacement the displacement. (0,0) is assumed, if null.
+	 * @return a polyline 
+	 * @throws IllegalArgumentException thrown if {@code mv} is null
+	 * @throws IllegalArgumentException thrown if {@code ws} is null
+	 */
+	public Path2D project(MapView mv, WaySlice ws, Point displacement) throws IllegalArgumentException{
+		checkArgNotNull(mv, "mv");
+		if (displacement == null) displacement = new Point(0,0);
+		Path2D polyline = project(mv, ws);
+	    AffineTransform at = new AffineTransform();
+	    at.setToTranslation(displacement.x, displacement.y);
+	    polyline = new Path2D.Float(polyline, at);		
+	    return polyline;
+	}
+	
+	protected void paintHelperLinesFromDragSourceToDraggedWaySlice(Graphics2D g, MapView mv){
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		if (model == null) return;
+		if (! model.isDragging()) return;
+		WaySlice dragSource = model.getDragSource();
+		
+		Node lowerTearOffNode = dragSource.getStartTearOffNode();
+		Node upperTearOffNode = dragSource.getEndTearOffNode();
+		Point offset = model.getDragOffset();
+	
+		// init the graphics attributes
+		float[] dashPattern = { 2, 3, 2, 3 };
+		g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND,1f, dashPattern,0f));
+		boolean crossing = helperLinesAreCrossing(mv, dragSource, offset);
+		if (lowerTearOffNode != null){
+			Point p1 = mv.getPoint(!crossing ? dragSource.getStartNode() : dragSource.getEndNode());
+			p1 = new Point(p1.x + offset.x, p1.y + offset.y);
+			Point p2 = mv.getPoint(lowerTearOffNode);	
+			g.drawLine(p1.x,p1.y, p2.x,p2.y);
+		}
+		if (upperTearOffNode != null){
+			Point p1 = mv.getPoint(!crossing ? dragSource.getEndNode() : dragSource.getStartNode());
+			p1 = new Point(p1.x + offset.x, p1.y + offset.y);
+			Point p2 = mv.getPoint(upperTearOffNode);	
+			g.drawLine(p1.x,p1.y, p2.x,p2.y);			
+		}	
+	}
+	
+	/**
+	 * <p>Checks whether the two helper lines from the drag source to the drop target 
+	 * intersect, because the ways of the drag source and the drop target don't have
+	 * the same direction.</p>
+	 * 
+	 * <p>If the helpers intersect, we will reverse the two end points of the drop target,
+	 * when we paint the helper lines.</p>
+	 *
+	 * @param mv the map view
+	 * @param dragSource 
+	 * @param dropTarget
+	 * @return true, if the two helper lines from the drag source to the drop target
+	 * intersect
+	 */
+	protected boolean helperLinesAreCrossing(MapView mv, WaySlice dragSource, WaySlice dropTarget){
+		Node s1 = dragSource.getStartTearOffNode();
+		if (s1 == null) s1 = dragSource.getStartNode();
+		Node s2= dragSource.getEndTearOffNode();
+		if (s2 == null) s2 = dragSource.getEndNode();
+		Point sp1 = mv.getPoint(s1);
+		Point sp2 = mv.getPoint(s2);
+		
+		Point tp1 = mv.getPoint(dropTarget.getStartNode());
+		Point tp2 = mv.getPoint(dropTarget.getEndNode());
+		return helperLinesAreCrossing(sp1,sp2,tp1,tp2);
+	}
+	
+	protected boolean helperLinesAreCrossing(Point s1, Point s2, Point t1, Point t2){
+		Line2D l1 = new Line2D.Float(s1.x, s1.y, t1.x,t1.y);
+		Line2D l2 = new Line2D.Float(s2.x, s2.y, t2.x,t2.y);
+		return l1.intersectsLine(l2);		
+	}
+	
+	/**
+	 * <p>Checks whether the two helper lines from the drag source to the currently 
+	 * painted drag object offset by {@code dragOffset} 
+	 * intersect.</p>
+	 * 
+	 * <p>If the helpers intersect, we will reverse the two end points of the drop target,
+	 * when we paint the helper lines.</p>
+	 *
+	 * @param mv the map view
+	 * @param dragSource 
+	 * @param dropTarget
+	 * @return true, if the two helper lines from the drag source to the drop target
+	 * intersect
+	 */
+	protected boolean helperLinesAreCrossing(MapView mv, WaySlice dragSource, Point dragOffset){
+		Node s1 = dragSource.getStartTearOffNode();
+		if (s1 == null) s1 = dragSource.getStartNode();
+		Node s2= dragSource.getEndTearOffNode();
+		if (s2 == null) s2 = dragSource.getEndNode();
+		Point sp1 = mv.getPoint(s1);
+		Point sp2 = mv.getPoint(s2);
+		
+		Point tp1 = mv.getPoint(dragSource.getStartNode());
+		tp1 = new Point(tp1.x + dragOffset.x, tp1.y + dragOffset.y);
+		
+		Point tp2 = mv.getPoint(dragSource.getEndNode());
+		tp2 = new Point(tp2.x + dragOffset.x, tp2.y + dragOffset.y);
+		
+		return helperLinesAreCrossing(sp1,sp2,tp1,tp2);
+	}
+	
+	protected void paintHelperLinesFromDragSourceToDropTarget(Graphics2D g, MapView mv){
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		if (model == null) return;
+		if (! model.isDragging()) return;
+		WaySlice dragSource = model.getDragSource();
+		WaySlice dropTarget = model.getDropTarget();
+		Node lowerTearOffNode = dragSource.getStartTearOffNode();		
+		Node upperTearOffNode = dragSource.getEndTearOffNode();
+		
+			
+		// init the graphics attributes
+		float[] dashPattern = { 2, 3, 2, 3 };
+		g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND,1f, dashPattern,0f));
+		
+		boolean crossing = helperLinesAreCrossing(mv, dragSource, dropTarget);
+		if (lowerTearOffNode != null){			
+			Point p1 = mv.getPoint(lowerTearOffNode);
+			Point p2 = mv.getPoint(!crossing ? dropTarget.getStartNode() : dropTarget.getEndNode());	
+			g.drawLine(p1.x,p1.y, p2.x,p2.y);
+		}
+		if (upperTearOffNode != null){
+			Point p1 = mv.getPoint(upperTearOffNode);
+			Point p2 = mv.getPoint(!crossing ? dropTarget.getEndNode() : dropTarget.getStartNode());	
+			g.drawLine(p1.x,p1.y, p2.x,p2.y);			
+		}	
+	}
+	
+	protected void paintDraggedWaySlice(Graphics2D g, MapView mv, Bounds bbox) {
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		if (model == null) return;
+		if (! model.isDragging()) return;
+		WaySlice dragSource = model.getDragSource();
+		WaySlice dropTarget = model.getDropTarget();
+		if (dragSource == null) return;		
+		if (dropTarget == null) {
+			/*
+			 * paint the temporary dragged way slice, unless the mouse is currently over a potential
+			 */
+			Path2D polyline = project(mv, dragSource, model.getDragOffset());
+			g.setColor(Color.RED);
+			float[] dashPattern = { 10, 5, 10, 5 };
+			g.setStroke(new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND,1f, dashPattern,0f));
+			g.draw(polyline);
+			paintHelperLinesFromDragSourceToDraggedWaySlice(g, mv);
+		} else {
+			/*
+			 * the mouse is over a suitable drop target. Paint only 
+			 * two helper lines from the drag source to the drop target. The drop target
+			 * is highlighted. 
+			 */
+			paintHelperLinesFromDragSourceToDropTarget(g,mv);			
+		}
+	}
+	
+		
+	/* -------------------------------------------------------------------------------- */
+	/* interface MapViewPaintable                                                       */
+	/* -------------------------------------------------------------------------------- */
+	@Override
+	public void paint(Graphics2D g, MapView mv, Bounds bbox) {
+		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
+		if (!ContourMergePlugin.isEnabled()) return;
+		ContourMergeModel model = ContourMergePlugin.getModelManager().getActiveModel();
+		if (model == null) return;
+		decorateSelectedNodes(g, mv, bbox);
+		decorateFeedbackNode(g, mv, bbox);		
+		WaySlice dragSourceSlice = model.getDragSource();
+		if (dragSourceSlice != null){
+			highlightWaySlice(g, mv, bbox, dragSourceSlice);
+		}
+		WaySlice dropTargetSlice = model.getDropTarget();
+		if (dropTargetSlice != null){
+			highlightWaySlice(g, mv, bbox, dropTargetSlice);
+		}
+		if (model.isDragging()){
+			paintDraggedWaySlice(g, mv, bbox);
+		}
+	}
+}
Index: /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/WaySlice.java
===================================================================
--- /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/WaySlice.java	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/WaySlice.java	(revision 24969)
@@ -0,0 +1,399 @@
+package org.openstreetmap.josm.plugins.contourmerge;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.plugins.contourmerge.util.Assert;
+
+/**
+ * <p>A <strong>WaySlice</strong> is a sub sequence of a ways sequence of nodes.</p>
+ * 
+ */
+public class WaySlice {
+	static private final Logger logger = Logger.getLogger(WaySlice.class.getName());
+
+	private Way w;
+	private int start;
+	private int end;
+	private boolean inDirection = true;
+			
+	/**
+	 * <p>Creates a new way slice for the way {@code w}. It consists of the nodes at the positions
+	 * <code>[start, start+1, ..., end]</code>.</p>
+	 * 
+	 * @param w the way. Must not be null.
+	 * @param start the index of the start node. 0 <= start < w.getNodeCount(). start < end
+	 * @param end the index of the end node. 0 <= end < w.getNodeCount(). start < end
+	 * @throws IllegalArgumentException thrown if one of the arguments isn't valid
+	 */
+	public WaySlice(Way w, int start, int end) throws IllegalArgumentException {
+		Assert.checkArgNotNull(w, "w");
+		Assert.checkArg(start >= 0 && start < w.getNodesCount(), "start out of range, got {0}", start);
+		Assert.checkArg(end >= 0 && end < w.getNodesCount(), "end out of range, got {0}", start);
+		Assert.checkArg(start < end, "expected start < end, got start={0}, end={1}", start, end);
+		this.w = w;
+		this.start = start;		
+		this.end = end;
+	}
+	
+	/**
+	 * <p>Creates a new way slice for the way {@code w}.</p>
+	 * 
+	 * <p>If {@code inDirection==true}, it consists of the nodes at the positions
+	 * <code>[start, start+1, ..., end]</code>.</p>
+	 * 
+	 * <p>If {@code inDirection==false} <strong>and w is {@link Way#isClosed() closed}</strong>, 
+	 * it consists of the nodes at the positions <code>[end, end+1,...,0,1,...,start]</code>.</p>  
+	 * @param w the way. Must not be null. 
+	 * @param start the index of the start node. 0 <= start < w.getNodeCount(). start < end
+	 * @param end the index of the end node. 0 <= end < w.getNodeCount(). start < end
+	 * @param inDirection true, this way slice is given by the nodes <code>[start, ..., end]</code>; false, if
+	 * is  given by the nodes <code>[end,..,0,..,start]</code> (provided the way is closed)
+	 * @throws IllegalArgumentException thrown if a precondition is violated 
+	 */
+	public WaySlice(Way w, int start, int end, boolean inDirection) throws IllegalArgumentException{
+		this(w,start,end);
+		if (!inDirection){
+			Assert.checkArg(w.isClosed(), "inDirection=false only supported provided w is closed");
+		}
+		if (w.isClosed() && start == 0 && end == w.getNodesCount() -1){
+			Assert.checkArg(false, "for a closed way, start and end must not both refer to the shared 'join'-node");
+		}
+		this.inDirection = inDirection;
+	}
+	
+	/**
+	 * Replies the way this is a slice of.
+	 * 
+	 * @return the way this is a slice of.
+	 */
+	public Way getWay() {
+		return w;
+	}
+	
+	/**
+	 * Replies the index of the first node of this way slice.
+	 * 
+	 * @return the index of the first node of this way slice
+	 */
+	public int getStart() {
+		return start;
+	}
+	
+	/**
+	 * Replies the index of the last node of this way slice.
+	 * 
+	 * @return the index of the first node of this way slice
+	 */
+	public int getEnd() {
+		return end;
+	}
+	
+	/**
+	 * Replies true, if this way slice has the same direction and the
+	 * parent way. Replies false, if it has the opposite direction.
+	 * 
+	 * @return
+	 */
+	public boolean isInDirection() {
+		return inDirection;
+	}
+	
+	public Node getStartNode(){
+		return w.getNode(start);
+	}
+	
+	public Node getEndNode() {
+		return w.getNode(end);
+	}
+	
+	/**
+	 * <p>Replies the lower node idx of the node from which this way slice is torn off.</p>
+	 * 
+	 * 
+	 * <strong>Example</strong>
+	 * <pre>
+	 *     n0 ------------- n1 ---------- n2 --------- n3 -------------- n4
+	 *                    start                       end 
+	 *     ^-- this is the lower position where the way slice [n1,n2,n3] is torn off  
+	 *     ==> the method replies 0             
+	 * </pre>
+	 * 
+	 * <p>Replies -1, if there is no such index.</p>
+	 *  
+	 * <strong>Example</strong>
+	 * <pre>
+	 *     n0 ------------- n1 ---------- n2 --------- n3 -------------- n4
+	 *     start                         end 
+	 *     The way slice starts at the first node of an open way => there is
+	 *     no node where the way slice is torn off
+	 *     ==> the method replies -1             
+	 * </pre>
+	 * 
+	 * @return
+	 */
+	public int getStartTearOffIdx() {
+		if (! w.isClosed()) {  // an open way 
+			return start > 0 ? start - 1 : -1;
+		} else {               // a closed way			
+			if (isInDirection()){			
+				int lower = start - 1;
+				if (lower < 0) lower = w.getNodesCount() - 2;
+				return lower == end ? -1 : lower;
+			} else {
+				int lower = end - 1;
+				if (lower < 0) lower = w.getNodesCount() - 2;
+				return lower == start ? -1 : lower;
+			}
+		}
+	}
+	
+	/**
+	 * <p>Replies the lower node from which this way slice is torn off, see
+	 * {@link #getStartTearOffIdx()} for more details.</p>
+
+	 * @return 
+	 */
+	public Node getStartTearOffNode() {
+		int i = getStartTearOffIdx();
+		return i==-1 ? null : w.getNode(i);
+	}
+	
+	/**
+	 * <p>Replies the upper node idx of the node from which this way slice is torn off.</p>
+	 * 
+	 * <strong>Example</strong>
+	 * <pre>
+	 *     n0 ------------- n1 ---------- n2 --------- n3 -------------- n4
+	 *                    start=====================  end 
+	 *                                                                   ^
+	 *                                                                   |
+	 *     this is the upper position where the way slice [n1,n2,n3] is torn off  
+	 *     ==> the method replies 4             
+	 * </pre>
+	 * 
+	 * <p>Replies -1, if there is no such index.</p> 
+	 * <strong>Example</strong>
+	 * <pre>
+	 *     n0 ------------- n1 ---------- n2 --------- n3 -------------- n4
+	 *                                    start ===================     end 
+	 *     The way slice ends at the last node of an open way => there is
+	 *     no node where the way slice is torn off
+	 *     ==> the method replies -1             
+	 * </pre>
+	 * 	 
+	 * @return
+	 */
+	public int getEndTearOffIdx() {
+		if (! w.isClosed()) {	  // an open way		
+			return end < w.getNodesCount()-1 ? end + 1 : -1;
+		} else {                  // a closed way 
+			if (inDirection) {
+				int upper = end + 1;
+				if (upper >= w.getNodesCount()-1) upper = 0;
+				return upper == start ? -1 : upper;
+			} else {
+				int upper = start + 1;
+				if (upper >= w.getNodesCount()-1) upper = 0;
+				return upper == end ? -1 : upper;
+			}
+		}
+	}
+	
+	/**
+	 * <p>Replies the upper node from which this way slice is torn off, see
+	 * {@link #getEndTearOffIdx()} for more details.</p>
+
+	 * @return 
+	 */
+	public Node getEndTearOffNode() {
+		int i = getEndTearOffIdx();
+		return i == -1 ? null : w.getNode(i);
+	}
+	
+	/**
+	 * <p>Replies the number of way segments in this way slice.</p>
+	 * 
+	 * @return the number of way segments in this way slice
+	 */
+	public int getNumSegments() {
+		if (inDirection) return end - start;
+		return start + (w.getNodesCount() - 1 - end); // for closed ways 
+	}
+	
+	/**
+	 * <p>Replies the opposite way slice, or null, if this way slice doesn't have
+	 * an opposite way slice, because it is a way slice in an open way.</p>
+	 * 
+	 * @return the oposite way slice 
+	 */
+	public WaySlice getOpositeSlice(){
+		if (!w.isClosed()) return null;
+		return new WaySlice(w, start, end, !inDirection);
+	}
+	
+	/**
+	 * <p>Replies a clone of the underlying way, where the nodes given by this way slice are
+	 * replaced with the nodes in {@code newNodes}.</code>
+	 * 
+	 * @param newNodes the new nodes. Ignored if null.
+	 * @return the cloned way with the new nodes 
+	 */
+	public Way replaceNodes(List<Node> newNodes) {
+		Way nw = new Way(w);
+		if (newNodes == null || newNodes.isEmpty()) return nw;
+		
+		if (!w.isClosed()) {
+			List<Node> oldNodes = new ArrayList<Node>(w.getNodes());
+			for (int i=start; i<= end;i++) oldNodes.remove(start);
+			oldNodes.addAll(start, newNodes);
+			nw.setNodes(oldNodes);
+		} else {
+			List<Node> oldNodes = new ArrayList<Node>(w.getNodes());
+			if (inDirection) {
+				if (start == 0)oldNodes.remove(oldNodes.size()-1);
+				for (int i=start; i<= end;i++)oldNodes.remove(start);
+				oldNodes.addAll(start, newNodes);
+				if (start == 0) oldNodes.add(newNodes.get(0));
+				nw.setNodes(oldNodes);
+			} else {
+				int upper = oldNodes.size()-1;
+				for (int i=end; i<=upper; i++) oldNodes.remove(end);
+				for (int i=0; i<=start;i++) oldNodes.remove(0);
+				oldNodes.addAll(0, newNodes);
+				oldNodes.add(newNodes.get(0));  // make sure the new way is closed again
+				nw.setNodes(oldNodes);
+			}
+		}
+		return nw;
+	}
+		
+	/**
+	 * <p>Replies the list of nodes, always starting at the start index, following the nodes
+	 * in the appropriate direction to the end index.</p>
+	 * 
+	 * @return the list of nodes
+	 */
+	public List<Node> getNodes(){
+		List<Node> nodes = new ArrayList<Node>();
+		if (!w.isClosed()) {
+			nodes.addAll(w.getNodes().subList(start, end+1));
+		} else {
+			if (inDirection) {
+				nodes.addAll(w.getNodes().subList(start, end+1));
+			} else {
+				for (int i=start; i>=0; i--) nodes.add(w.getNode(i));
+				// do not add the last node which is the join node common to the node at index 0
+				for (int i=w.getNodesCount()-2; i>=end; i--) nodes.add(w.getNode(i));
+			}
+		}
+		return nodes;
+	}
+	
+	
+	/**
+	 * Replies true if this way slice participates in at least one sling.
+	 * Here's an example of such a sling.
+	 * <pre>
+	 *                  5
+	 *                  |
+	 *                  | 
+	 *    1======2======3=======4
+	 *                  |       |
+	 *                  |       |
+	 *                  7-------6
+	 * </pre>
+	 * <ul>
+	 *   <li>the nodes [3,4,6,7] form a sling in the way. Note that the way itself is not <em>closed</em>,
+	 *   {@link Way#isClosed() isClosed()} will return false.</li>
+	 *   <li>the way slice [1,2,3,4] <strong>does</strong> participate in a sling</li>
+	 *   <li>the way slice [1,2] <strong>doesn't</strong> participate in a sling</li>
+	 * </ul>
+	 * 
+	 * @return true if this way slice participates in at least one sling.
+	 */
+	protected boolean hasSlings() {
+		Set<Node> nodeSet = new HashSet<Node>();
+		if (w.isClosed()){
+			if (isInDirection()) {
+				for (int i=start; i<=end; i++){
+					nodeSet.add(w.getNode(i));
+				}
+			} else {
+				/* A way slice including the common start/end node of a closed way.
+				 * Make sure we look only once at the common start/end node.
+				 */
+				for (int i=start; i > 0 /* don't add the the start node */; i--){
+					nodeSet.add(w.getNode(i));
+				}
+				for (int i=w.getNodesCount()-1 /* add the end node */; i >= end; i--){
+					nodeSet.add(w.getNode(i));
+				}
+			}
+		} else {
+			for (int i=start; i<=end; i++){
+				nodeSet.add(w.getNode(i));
+			}			
+		}
+		/*
+		 * make sure each node in  way slice occurs exactly once in the way. This ensures
+		 * that the way slice is not participating in any slings. 
+		 */
+		Set<Node> seen = new HashSet<Node>();
+		for (int i=0; i< (w.isClosed() ? w.getNodesCount()-1 : w.getNodesCount()); i++) {
+			Node n = w.getNode(i);
+			if (seen.contains(n)) return true;
+			if (nodeSet.contains(n)) seen.add(n);
+		}
+		return false;
+	}
+	
+	public String toString() {
+		StringBuilder sb = new StringBuilder();
+		sb.append("<way-slice ").append("way=").append(w.getPrimitiveId())
+			.append(", start=").append(start)
+			.append(", end=").append(end)
+			.append(", isInDirection=").append(isInDirection())
+			.append(">");
+		return sb.toString();
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + end;
+		result = prime * result + (inDirection ? 1231 : 1237);
+		result = prime * result + start;
+		result = prime * result + ((w == null) ? 0 : w.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		WaySlice other = (WaySlice) obj;
+		if (end != other.end)
+			return false;
+		if (inDirection != other.inDirection)
+			return false;
+		if (start != other.start)
+			return false;
+		if (w == null) {
+			if (other.w != null)
+				return false;
+		} else if (!w.equals(other.w))
+			return false;
+		return true;
+	}
+}
Index: /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/util/Assert.java
===================================================================
--- /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/util/Assert.java	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/src/org/openstreetmap/josm/plugins/contourmerge/util/Assert.java	(revision 24969)
@@ -0,0 +1,29 @@
+package org.openstreetmap.josm.plugins.contourmerge.util;
+
+import java.text.MessageFormat;
+
+public class Assert {
+	public static void checkArgNotNull(Object arg, String name) throws IllegalArgumentException {
+		if (arg == null){
+			throw new IllegalArgumentException(
+					MessageFormat.format("argument ''{0}'' must not be null", name) // don't translate, it's a technical message
+			);
+		}
+	}
+	
+	public static void checkArg(boolean cond, String msg, Object... values){
+		if (!cond){
+			throw new IllegalArgumentException(
+					MessageFormat.format(msg, values)
+			);
+		}
+	}
+	
+	public static void assertTrue(boolean cond, String msg, Object... values) throws AssertionError {
+		if (!cond){
+			throw new AssertionError(
+					MessageFormat.format(msg, values)
+			);
+		}
+	}
+}
Index: /applications/editors/josm/plugins/contourmerge/test/config/test-unit-env.properties
===================================================================
--- /applications/editors/josm/plugins/contourmerge/test/config/test-unit-env.properties	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/test/config/test-unit-env.properties	(revision 24969)
@@ -0,0 +1,11 @@
+#
+# This file includes properties which are used by all contouralign unit tests 
+#
+
+#### 
+# josm.home - the home directory of the JOSM installation to be used in unit tests
+#
+# This is the home directory for JOSM preferences: ${josm.home}\preferences
+# This is the home directory for JOSM plugins: ${josm.home}\plugins\*.jar
+#
+josm.home=C:\\data\\eclipse-ws\\eclipse-3.6.1\\JOSM plugins\\plugins\\contourmerge\\test\\josm.home
Index: /applications/editors/josm/plugins/contourmerge/test/data/lake-and-forest.osm
===================================================================
--- /applications/editors/josm/plugins/contourmerge/test/data/lake-and-forest.osm	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/test/data/lake-and-forest.osm	(revision 24969)
@@ -0,0 +1,95 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<osm version='0.6' generator='JOSM'>
+  <node id='-90' visible='true' lat='1.9498085934871392' lon='0.34638020265185143' />
+  <node id='-88' visible='true' lat='1.638773853649603' lon='0.9546107120454161' />
+  <node id='-86' visible='true' lat='0.7619913507395978' lon='1.4638269524679355' />
+  <node id='-84' visible='true' lat='-0.46858687661857246' lon='1.2375086233912602' />
+  <node id='-82' visible='true' lat='-1.614115094458116' lon='0.8555964430743708' />
+  <node id='-80' visible='true' lat='-1.8686054230914202' lon='-0.4032992624146352' />
+  <node id='-78' visible='true' lat='-1.1191861174527094' lon='-0.10625645550149895' />
+  <node id='-76' visible='true' lat='-0.5110200117970292' lon='-0.07796666436691468' />
+  <node id='-74' visible='true' lat='-0.05839011879480225' lon='-0.3467196801454665' />
+  <node id='-72' visible='true' lat='0.7478476829852718' lon='-0.5306033225202651' />
+  <node id='-71' visible='true' lat='1.3135496610746913' lon='-0.2194156200398366' />
+  <node id='-68' visible='true' lat='-1.9309630062596987' lon='-1.1703619409481616' />
+  <node id='-66' action='modify' visible='true' lat='-1.8673429658648064' lon='-0.9812122994980214' />
+  <node id='-64' visible='true' lat='-1.5675432920105192' lon='-1.100362407149425' />
+  <node id='-62' visible='true' lat='-1.33672609583098' lon='-1.0669345607465544' />
+  <node id='-60' visible='true' lat='-1.025555382429284' lon='-0.7004779107301904' />
+  <node id='-58' visible='true' lat='-1.2470406316824045' lon='-0.8945805119703188' />
+  <node id='-56' visible='true' lat='1.134452647559947' lon='-1.0985646840056646' />
+  <node id='-54' action='modify' visible='true' lat='0.92149205517992' lon='-1.1310786069615206' />
+  <node id='-52' visible='true' lat='0.2510093396694106' lon='-1.165078156915072' />
+  <node id='-49' visible='true' lat='0.5475788063904028' lon='-1.1548513638215931' />
+  <node id='-46' action='modify' visible='true' lat='0.09914780643557862' lon='-1.2150165595229268' />
+  <node id='-43' visible='true' lat='-0.6924676624556036' lon='-0.7865753752903196' />
+  <node id='-37' visible='true' lat='-0.23723034317940755' lon='-1.081739732375527' />
+  <node id='-35' action='modify' visible='true' lat='-0.3122823436555008' lon='-0.9686952470789261' />
+  <node id='-33' action='modify' visible='true' lat='-1.683740576638317' lon='-1.0420397507915253' />
+  <node id='-31' action='modify' visible='true' lat='-1.3983148886672743' lon='-1.1852971360881919' />
+  <node id='-29' action='modify' visible='true' lat='-1.1906003389711877' lon='-0.7861187826517283' />
+  <node id='-27' action='modify' visible='true' lat='-0.5113437945948224' lon='-0.946671137186523' />
+  <node id='-24' action='modify' visible='true' lat='1.3939833634667225' lon='-1.0682225132419065' />
+  <node id='-22' action='modify' visible='true' lat='0.7187681598611815' lon='-1.2076346039531383' />
+  <node id='-20' action='modify' visible='true' lat='-0.07604293624534264' lon='-1.097191259363721' />
+  <node id='-18' action='modify' visible='true' lat='-0.9142872452356418' lon='-0.6427440545777575' />
+  <node id='-16' action='modify' visible='true' lat='-2.117857121793444' lon='-1.2673826428293806' />
+  <node id='-14' action='modify' visible='true' lat='-1.7740521649134848' lon='-2.05497042801621' />
+  <node id='-12' action='modify' visible='true' lat='-1.621919963647345' lon='-2.1743533466041565' />
+  <node id='-10' action='modify' visible='true' lat='-0.47073682851158927' lon='-2.7339254152462362' />
+  <node id='-8' action='modify' visible='true' lat='0.3168440458154412' lon='-3.186562073399586' />
+  <node id='-6' action='modify' visible='true' lat='1.3034811217373394' lon='-3.1322456744211844' />
+  <node id='-4' action='modify' visible='true' lat='1.547829070274015' lon='-2.761083614735437' />
+  <node id='-3' action='modify' visible='true' lat='1.602124947244056' lon='-1.8648630315918029' />
+  <way id='-73' action='modify' visible='true'>
+    <nd ref='-71' />
+    <nd ref='-72' />
+    <nd ref='-74' />
+    <nd ref='-76' />
+    <nd ref='-78' />
+    <nd ref='-80' />
+    <nd ref='-82' />
+    <nd ref='-84' />
+    <nd ref='-86' />
+    <nd ref='-88' />
+    <nd ref='-90' />
+    <nd ref='-71' />
+    <tag k='landuse' v='forest' />
+    <tag k='name' v='a forest' />
+  </way>
+  <way id='-5' action='modify' visible='true'>
+    <nd ref='-3' />
+    <nd ref='-4' />
+    <nd ref='-6' />
+    <nd ref='-8' />
+    <nd ref='-10' />
+    <nd ref='-12' />
+    <nd ref='-14' />
+    <nd ref='-16' />
+    <nd ref='-68' />
+    <nd ref='-66' />
+    <nd ref='-33' />
+    <nd ref='-64' />
+    <nd ref='-31' />
+    <nd ref='-62' />
+    <nd ref='-58' />
+    <nd ref='-29' />
+    <nd ref='-60' />
+    <nd ref='-18' />
+    <nd ref='-43' />
+    <nd ref='-27' />
+    <nd ref='-35' />
+    <nd ref='-37' />
+    <nd ref='-20' />
+    <nd ref='-46' />
+    <nd ref='-52' />
+    <nd ref='-49' />
+    <nd ref='-22' />
+    <nd ref='-54' />
+    <nd ref='-56' />
+    <nd ref='-24' />
+    <nd ref='-3' />
+    <tag k='name' v='a lake' />
+    <tag k='natural' v='water' />
+  </way>
+</osm>
Index: /applications/editors/josm/plugins/contourmerge/test/josm.home/preferences
===================================================================
--- /applications/editors/josm/plugins/contourmerge/test/josm.home/preferences	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/test/josm.home/preferences	(revision 24969)
@@ -0,0 +1,1 @@
+osm-server.url=http://localhost:8080/api
Index: /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/AllUnitTests.groovy
===================================================================
--- /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/AllUnitTests.groovy	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/AllUnitTests.groovy	(revision 24969)
@@ -0,0 +1,10 @@
+package org.openstreetmap.josm.plugins.contourmerge
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses([
+	ContourMergeModelTest.class,
+	WaySliceTest.class
+])
+class AllUnitTests {}
Index: /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModelTest.groovy
===================================================================
--- /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModelTest.groovy	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/ContourMergeModelTest.groovy	(revision 24969)
@@ -0,0 +1,487 @@
+package org.openstreetmap.josm.plugins.contourmerge;
+
+import java.util.Arrays;
+
+import groovy.lang.GroovyInterceptable;
+import groovy.util.GroovyTestCase;
+
+import static org.junit.Assert.*;
+import org.junit.*;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.plugins.contourmerge.fixtures.JOSMFixture;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.Command;
+
+class ContourMergeModelTest {
+
+	def shouldFail = new GroovyTestCase().&shouldFail
+
+	private def DataSet ds
+	
+	def newNodes(Range nodes){
+		def nn = []
+		nodes.each {i->
+			nn << newNode(i)
+		}
+		return nn
+	}	
+	def newNode(int id){
+		Node n = new Node(id)
+		ds.addPrimitive(n)
+		return n
+	}
+	
+	def newWay(int id, Node... nodes){
+		return newWay(id,Arrays.asList(nodes))
+	}
+	
+	def newWay(int id, List<Node> nodes){
+		Way w = new Way(id,1)
+		w.setNodes(nodes)
+		ds.addPrimitive(w)
+		return w
+	}
+	
+	def newWay(int id, Range nodes) {
+		def nn = []
+		nodes.each {i->
+			Node n = ds.getPrimitiveById(i, OsmPrimitiveType.NODE)
+			if (n == null) {
+				n = newNode(i)
+			}	
+			nn << n
+		}
+		return newWay(id,nn)
+	}
+	
+	def newWay(args){
+		Way w = newWay(args["id"].toInteger(), args["nodes"])
+		if (args["closed"]){
+			w.setNodes(w.getNodes() + w.getNode(0))
+		}
+		return w
+	}
+	
+	def getProperty(String name){
+		switch(name){
+			case ~/^n(\d+)$/:
+				def m = name =~ /^n(\d+)$/ 
+				return ds.getPrimitiveById(m[0][1].toInteger(),OsmPrimitiveType.NODE)
+ 
+			 case ~/^w(\d+)$/:
+				def m = name=~ /^w(\d+)$/ 
+				return ds.getPrimitiveById(m[0][1].toInteger(),OsmPrimitiveType.WAY)
+		}
+		return getMetaClass().getProperty(this,name)
+	}
+	
+	def createModelMock(){
+		OsmDataLayer layer = new OsmDataLayer(ds, null, null)
+		ContourMergeModel model = new ContourMergeModel(layer)
+		return model
+	}
+	
+	@BeforeClass
+	static public void startJOSMFixture() {
+		JOSMFixture.createUnitTestFixture().init()
+	}
+	
+	@Before 
+	public void setUp() {
+		ds = new DataSet()
+	}
+		
+	@Test
+	public void selectNode() {
+		ContourMergeModel model = createModelMock()
+		Node n = newNode(1)
+		model.selectNode(n)
+		assert model.isSelected(n)
+		model.deselectNode(n)
+		assert !model.isSelected(n)		
+	}
+	
+	@Test
+	public void selectNodeAndDeselectAll() {
+		ContourMergeModel model = createModelMock()
+		model.selectNode(newNode(1))
+		model.selectNode(newNode(2))
+		assert model.getSelectedNodes().size() == 2
+		model.deselectAllNodes()
+		assert model.getSelectedNodes().isEmpty()
+	}
+	
+	@Test
+	public void toggleSelected(){
+		ContourMergeModel model = createModelMock()
+		Node n = newNode(1)
+		model.selectNode(n)
+		model.toggleSelected(n)
+		assert !model.isSelected(n)
+		model.toggleSelected(n)
+		assert model.isSelected(n)
+	}
+	
+
+	@Test	
+	public void getDragSource_OpenWayNoSelectedNodes() {
+		Node n1 = newNode(1)
+		Node n2 = newNode(2)
+		Node n3 = newNode(3)
+		Node n4 = newNode(4)
+		Way w = newWay(1,n1,n2,n3,n4)
+		
+		WaySegment ws = new WaySegment(w, 1)
+		ContourMergeModel model = createModelMock()
+		model.setDragStartFeedbackWaySegment(ws)
+		WaySlice slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 0
+		assert slice.end == w.nodesCount-1
+
+		ws = new WaySegment(w, 0)
+		model = createModelMock()
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 0
+		assert slice.end == w.nodesCount-1
+		
+		ws = new WaySegment(w, 2)
+		model = createModelMock()
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 0
+		assert slice.end == w.nodesCount-1
+	}
+
+	@Test
+	public void getDragSource_OpenWayOneSelectedNode() {
+		Node n1 = newNode(1)
+		Node n2 = newNode(2)
+		Node n3 = newNode(3)
+		Node n4 = newNode(4)
+		Way w = newWay(1,n1,n2,n3,n4)
+		WaySegment ws
+		ContourMergeModel model
+		WaySlice slice
+		
+		/*
+		 * n1----------n2---------n3-----------n4
+		 *             x                              ~selected
+		 *       ^                                    ~drag start
+		 */
+		model = createModelMock()
+		model.selectNode(n2)
+		ws = new WaySegment(w, 0)				
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 0
+		assert slice.end == 1
+		
+		/*
+		* n1----------n2---------n3-----------n4
+		*             x                            ~selected
+		*                 ^                        ~drag start 
+		*/
+		model = createModelMock()
+		model.selectNode(n2)
+		ws = new WaySegment(w, 1)
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 1
+		assert slice.end == 3
+		
+		/*
+		* n1----------n2---------n3-----------n4
+		*             x                           ~selected
+		*                                ^        ~drag start
+		*/
+		model = createModelMock()
+		model.selectNode(n2)
+		ws = new WaySegment(w, 2)
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 1
+		assert slice.end == 3
+	}
+	
+	@Test
+	public void getDragSource_OpenWayFirstOrLastSelectedNode() {
+		Node n1 = newNode(1)
+		Node n2 = newNode(2)
+		Node n3 = newNode(3)
+		Node n4 = newNode(4)
+		Way w = newWay(1,n1,n2,n3,n4)
+		WaySegment ws
+		ContourMergeModel model
+		WaySlice slice
+		
+		/*
+		 * n1----------n2---------n3-----------n4
+		 * x                                          ~selected
+		 *                     ^                      ~drag start
+		 */
+		model = createModelMock()
+		model.selectNode(n1)
+		ws = new WaySegment(w, 1)
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 0
+		assert slice.end == 3
+		
+		/*
+		* n1----------n2---------n3-----------n4
+		*                                      x   ~selected
+		*                 ^                        ~drag start
+		*/
+		model = createModelMock()
+		model.selectNode(n4)
+		ws = new WaySegment(w, 1)
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 0
+		assert slice.end == 3
+	}
+	
+	@Test
+	public void getDragSource_ClosedWayTwoSelectedNodes() {
+		Node n1 = newNode(1)
+		Node n2 = newNode(2)
+		Node n3 = newNode(3)
+		Node n4 = newNode(4)
+		Node n5 = newNode(5)
+		Way w = newWay(1,n1,n2,n3,n4,n5,n1)
+		WaySegment ws
+		ContourMergeModel model
+		WaySlice slice
+		
+		/*
+		 *+------------------------------------------------+
+		* |                                                |
+		* n1----------n2---------n3-----------n4-----------n5
+		*             x                       x                ~selected
+		*                    ^                                 ~drag start
+		*/
+		model = createModelMock()
+		model.selectNode(n2)
+		model.selectNode(n4)
+		ws = new WaySegment(w, 1)
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 1
+		assert slice.end == 3
+		
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   *             x                       x                ~selected
+	   *                             ^                        ~drag start
+	   */
+	   model = createModelMock()
+	   model.selectNode(n2)
+	   model.selectNode(n4)
+	   ws = new WaySegment(w, 1)
+	   model.setDragStartFeedbackWaySegment(ws)
+	   slice = model.getDragSource()
+	   assert slice != null
+	   assert slice.way == w
+	   assert slice.start == 1
+	   assert slice.end == 3
+	   
+	   /*
+	   *+------------------------------------------------+
+	  * |                                                |
+	  * n1----------n2---------n3-----------n4-----------n5
+	  *             x                       x                ~selected
+	  *                                          ^           ~drag start
+	  */
+	  model = createModelMock()
+	  model.selectNode(n2)
+	  model.selectNode(n4)
+	  ws = new WaySegment(w, 3)
+	  model.setDragStartFeedbackWaySegment(ws)
+	  slice = model.getDragSource()
+	  assert slice != null
+	  assert slice.way == w
+	  assert slice.start == 1
+	  assert slice.end == 3
+	  assert !slice.inDirection
+	  
+	  /*
+	  *+------------------------------------------------+
+	 * |                                                |
+	 * n1----------n2---------n3-----------n4-----------n5
+	 *             x                       x                ~selected
+	 *      ^                                               ~drag start
+	 */
+	 model = createModelMock()
+	 model.selectNode(n2)
+	 model.selectNode(n4)
+	 ws = new WaySegment(w, 0)
+	 model.setDragStartFeedbackWaySegment(ws)
+	 slice = model.getDragSource()
+	 assert slice != null
+	 assert slice.way == w
+	 assert slice.start == 1
+	 assert slice.end == 3
+	 assert !slice.inDirection
+	 
+		 /*
+		 *+------------------------------------------------+
+		* |                                                |
+		* n1----------n2---------n3-----------n4-----------n5
+		* x                      x                            ~selected
+		*        ^                                            ~drag start
+		*/
+		model = createModelMock()
+		model.selectNode(n1)
+		model.selectNode(n3)
+		ws = new WaySegment(w, 0)
+		model.setDragStartFeedbackWaySegment(ws)
+		slice = model.getDragSource()
+		assert slice != null
+		assert slice.way == w
+		assert slice.start == 0
+		assert slice.end == 2
+		assert slice.inDirection
+		
+	   /*
+	   * +------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   * x                      x                            ~selected
+	   *                                   ^                 ~drag start
+	   */
+	   model = createModelMock()
+	   model.selectNode(n1)
+	   model.selectNode(n3)
+	   ws = new WaySegment(w, 2)
+	   model.setDragStartFeedbackWaySegment(ws)
+	   slice = model.getDragSource()
+	   assert slice != null
+	   assert slice.way == w
+	   assert slice.start == 0
+	   assert slice.end == 2
+	   assert !slice.inDirection
+	   
+	   /*
+	    *             v ~drag start
+	   * +------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   * x                                                x   ~selected
+	   */
+	   model = createModelMock()
+	   model.selectNode(n1)
+	   model.selectNode(n5)
+	   ws = new WaySegment(w, 4)
+	   model.setDragStartFeedbackWaySegment(ws)
+	   slice = model.getDragSource()
+	   assert slice != null
+	   assert slice.way == w
+	   assert slice.start == 0
+	   assert slice.end == 4
+	   assert !slice.inDirection
+	}
+	
+	@Test
+	public void isWaySegmentDragable() {
+		Node n1 = newNode(1)
+		Node n2 = newNode(2)
+		Node n3 = newNode(3)
+		Node n4 = newNode(4)
+		Node n5 = newNode(5)
+		ContourMergeModel model
+
+		// an open way 
+		Way w = newWay(1,n1,n2,n3,n4)
+				
+		/*
+		 * n1----------n2---------n3-----------n4
+		 *                                            ~selected
+		 */
+		model = createModelMock()
+		(0..2).each {
+			// we can start a drag on each segment, even if no nodes are selected
+			assert model.isWaySegmentDragable(new WaySegment(w, it))
+		}
+
+		/*
+		* n1----------n2---------n3-----------n4
+		*             x                       x        ~selected
+		*/
+	   model = createModelMock()
+	   (0..2).each {
+		   // we can start a drag on each segment, regardless of the number
+		   // of selected nodes 
+		   assert model.isWaySegmentDragable(new WaySegment(w, it))
+	   }
+
+		// a closed way 
+		w = newWay(2,n1,n2,n3,n4,n5,n1)		
+		/*
+		 *+------------------------------------------------+
+		* |                                                |
+		* n1----------n2---------n3-----------n4-----------n5
+		*             x                       x                ~selected
+		*/
+		model = createModelMock()
+		model.selectNode(n2)		
+		model.selectNode(n4)
+		(0..4).each {
+			// we can start a drag operation on each segment
+			assert model.isWaySegmentDragable(new WaySegment(w, it))
+		}
+		
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   *             x                                        ~selected
+	   */
+	   model = createModelMock()
+	   model.selectNode(n2)
+	   (0..4).each {
+		   // we can start a drag operation on any segment, because there
+		   // is only one node selected
+		   assert ! model.isWaySegmentDragable(new WaySegment(w, it))
+	   }
+	   
+	   /*
+	   *+------------------------------------------------+
+	  * |                                                |
+	  * n1----------n2---------n3-----------n4-----------n5
+	  *                                                      ~selected
+	  */
+	  model = createModelMock()
+	  (0..4).each {
+		  // we can start a drag operation on any segment, because there
+		  // is no node selected
+		  assert ! model.isWaySegmentDragable(new WaySegment(w, it))
+	  }		
+	}
+ }
Index: /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/WaySliceTest.groovy
===================================================================
--- /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/WaySliceTest.groovy	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/WaySliceTest.groovy	(revision 24969)
@@ -0,0 +1,359 @@
+package org.openstreetmap.josm.plugins.contourmerge;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+import org.junit.*;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.plugins.contourmerge.fixtures.JOSMFixture;
+import org.openstreetmap.josm.plugins.contourmerge.WaySlice;
+
+class WaySliceTest {
+	def shouldFail = new GroovyTestCase().&shouldFail
+	
+	def newNode(id){
+		return new Node(id)
+	}
+	
+	def newWay(id, Node... nodes){
+		Way w = new Way(id,1)
+		w.setNodes(Arrays.asList(nodes))
+		return w
+	}
+	
+	def newWay(id, List<Node> nodes){
+		Way w = new Way(id,1)
+		w.setNodes(nodes)
+		return w
+	}
+	
+	@BeforeClass
+	static public void startJOSMFixtures() {
+		JOSMFixture.createUnitTestFixture().init()
+	}
+	
+	@Test
+	public void constructor_inDirection(){
+		def w = newWay(1, newNode(1), newNode(2), newNode(4), newNode(5))
+		
+		WaySlice ws 
+		ws = new WaySlice(w, 0, 1)		
+		assert ws.getWay() == w
+		assert ws.getStart() == 0
+		assert ws.getEnd() == 1
+		
+		ws = new WaySlice(w, 1, 3)
+		assert ws.getWay() == w
+		assert ws.getStart() == 1
+		assert ws.getEnd() == 3
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(null, 1, 3) // way must not be null
+		}
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, -1, 3)  // start index >= 0 required 
+		}
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 4, 3)   // start index < num nodes required 
+		}
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 0, -1)  // end index >= 0 required
+		}
+
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 0, 4)   // end index < num nodes required 
+		}
+
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 3, 2)  // start < end required 
+		}
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 3, 3)  // start < end required
+		}
+	}
+	
+	@Test
+	public void constructor_inOppositeDirection(){
+		def n = newNode(1)
+		// this is a closed way
+		def w = newWay(1, n, newNode(2), newNode(4), newNode(5), n)
+		
+		WaySlice ws
+		ws = new WaySlice(w, 0, 1, true)
+		assert ws.getWay() == w
+		assert ws.getStart() == 0
+		assert ws.getEnd() == 1
+		assert ws.isInDirection()
+		
+		ws = new WaySlice(w, 0, 1, false)
+		assert ws.getWay() == w
+		assert ws.getStart() == 0
+		assert ws.getEnd() == 1
+		assert ! ws.isInDirection()
+		
+		ws = new WaySlice(w, 1, 3, false)
+		assert ws.getWay() == w
+		assert ws.getStart() == 1
+		assert ws.getEnd() == 3
+		assert ! ws.isInDirection()
+		
+	
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(null, 1, 3,false) // way must not be null
+		}
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, -1, 3,false)  // start index >= 0 required
+		}
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 4, 3, false)   // start index < num nodes required
+		}
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 0, -1, false)  // end index >= 0 required
+		}
+
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 0, 4, false)   // end index < num nodes required
+		}
+
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 3, 2, false)  // start < end required
+		}
+		
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 3, 3, false)  // start < end required
+		}
+		
+		w = newWay(2, n, newNode(2), newNode(3), newNode(4))
+		shouldFail(IllegalArgumentException){
+			ws = new WaySlice(w, 0,1, false)  // way slice in opposite direction not allowed
+			                                  // for an open way
+		}
+	}
+	
+	@Test
+	public void getStartTearOffIndex() {
+		def w = newWay(1, newNode(1), newNode(2), newNode(3), newNode(4), newNode(5)) // an open way
+		
+		WaySlice ws
+		ws = new WaySlice(w, 2,3)
+		assert ws.getStartTearOffIdx() == 1
+		
+		ws = new WaySlice(w, 0, 3)
+		assert ws.getStartTearOffIdx() == -1 // no tear off node available
+		
+		def n = newNode(1)
+		w = newWay(1, n, newNode(2), newNode(3), newNode(4), newNode(5),n) // a closed way
+		
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   *                        x *********** x              ~ slice
+	   *             ^                                       ~ expected lower tear off index                        
+	   */
+		ws = new WaySlice(w, 2, 3)
+		assert ws.getStartTearOffIdx() == 1
+
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   * x *********** x                                       ~ slice
+	   *                                                  ^    ~ expected lower tear off index
+	   */
+		ws = new WaySlice(w, 0, 1)
+		assert ws.getStartTearOffIdx() == 4
+
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   * x ********************************************** x    ~ slice
+	   *                                                       ~ expected lower tear off index
+	   */
+		ws = new WaySlice(w, 0, 4)
+		assert ws.getStartTearOffIdx() == -1
+		
+		
+		/*
+		*    +------------------------------------------------+
+	   *     |                                                |
+	   *     n1----------n2---------n3-----------n4-----------n5
+	   * ****x                                                x***   ~ slice
+	   *                                          ^                  ~ expected upper tear off index
+	   */
+		ws = new WaySlice(w, 0, 4, false)
+		assert ws.getStartTearOffIdx() == 3
+	}
+	
+	
+	@Test
+	public void getEndTearOffIndex() {
+		def w = newWay(1, newNode(1), newNode(2), newNode(3),newNode(4), newNode(5)) // an open way
+		
+		WaySlice ws
+		ws = new WaySlice(w, 1,2)
+		assert ws.getEndTearOffIdx() == 3
+		
+		ws = new WaySlice(w, 0, 4)
+		assert ws.getEndTearOffIdx() == -1 // no tear off node available
+		
+		def n = newNode(1)
+		w = newWay(1, n, newNode(2), newNode(3), newNode(4), newNode(5),n) // a closed way
+		
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   *                        x *********** x              ~ slice
+	   *             ^                                       ~ expected upper tear off index
+	   */
+		ws = new WaySlice(w, 2, 3)
+		assert ws.getEndTearOffIdx() == 4
+		
+		
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   *                                     x *********** x  ~ slice
+	   * ^                                                    ~ expected upper tear off index
+	   */
+		ws = new WaySlice(w, 3, 4)
+		assert ws.getEndTearOffIdx() == 0
+
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   * x *********** x                                       ~ slice
+	   *                        ^                              ~ expected upper tear off index
+	   */
+		ws = new WaySlice(w, 0, 1)
+		assert ws.getEndTearOffIdx() == 2
+
+		/*
+		*+------------------------------------------------+
+	   * |                                                |
+	   * n1----------n2---------n3-----------n4-----------n5
+	   * x ********************************************** x    ~ slice
+	   *                                                       ~ expected upper tear off index
+	   */
+		ws = new WaySlice(w, 0, 4)
+		assert ws.getEndTearOffIdx() == -1
+		
+		
+		/*
+		*    +------------------------------------------------+
+	   *     |                                                |
+	   *     n1----------n2---------n3-----------n4-----------n5
+	   * ****x                                                x***   ~ slice
+	   *                  ^                                          ~ expected upper tear off index
+	   */
+		ws = new WaySlice(w, 0, 4, false)
+		assert ws.getEndTearOffIdx() == 1
+	}
+	
+	@Test
+	public void getNumSegments() {
+		def w = newWay(1, newNode(1), newNode(2), newNode(3), newNode(4), newNode(5)) // an open way
+		
+		def ws = new WaySlice(w, 0, 3)
+		assert ws.getNumSegments() == 3
+		
+		ws = new WaySlice(w, 1, 2)
+		assert ws.getNumSegments() == 1
+		
+		Node n = newNode(1)
+		w = newWay(1, n, newNode(2), newNode(3), newNode(4), newNode(5), n) // a closed way
+
+		ws = new WaySlice(w, 0, 3)
+		assert ws.getNumSegments() == 3
+		ws = ws.getOpositeSlice()
+		assert ws.getNumSegments() == 2
+		
+		ws = new WaySlice(w, 1, 2)
+		assert ws.getNumSegments() == 1
+		ws = ws.getOpositeSlice()
+		assert ws.getNumSegments() == 4
+	}
+	
+	
+	def  n(i){
+		return new Node(i)
+	}
+	
+	@Test
+	public void replaceNodes_OpenWay() {		
+		def w = newWay(1, newNode(1), newNode(2), newNode(3), newNode(4), newNode(5)) // an open way
+		def newnodes = [newNode(10), newNode(11), newNode(12)]
+		
+		def ws = new WaySlice(w, 1, 2)
+		Way wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(1), n(10), n(11), n(12), n(4), n(5)]
+		
+		ws = new WaySlice(w, 0, 2)
+		wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(10), n(11), n(12), n(4), n(5)]
+		
+		ws = new WaySlice(w, 3, 4)
+		wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(1),n(2), n(3), n(10), n(11), n(12)]
+
+		ws = new WaySlice(w, 0, 4)
+		wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(10), n(11), n(12)]
+	}
+	
+	@Test
+	public void replaceNodes_ClosedWay_InDirection() {
+		Node n1 = new Node(1)
+		def w = newWay(1, n1, newNode(2), newNode(3), newNode(4), newNode(5), n1) // a closed way
+		def newnodes = [newNode(10), newNode(11), newNode(12)]
+		
+		def ws = new WaySlice(w, 1, 2)
+		Way wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n1, n(10), n(11), n(12), n(4), n(5), n1]
+		
+		ws = new WaySlice(w, 0, 2)
+		wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(10), n(11), n(12), n(4), n(5), n(10)]
+		
+		ws = new WaySlice(w, 3, 4)
+		wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(1),n(2), n(3), n(10), n(11), n(12), n(1)]
+
+		ws = new WaySlice(w, 0, 4)
+		wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(10), n(11), n(12), n(10)]
+	}
+	
+	@Test
+	public void replaceNodes_ClosedWay_ReverseDirection() {
+		Node n1 = new Node(1)
+		def w = newWay(1, n1, newNode(2), newNode(3), newNode(4), newNode(5), n1) // a closed way
+		def newnodes = [newNode(10), newNode(11), newNode(12)]
+		
+		def ws = new WaySlice(w, 1, 2, false)
+		Way wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(10), n(11), n(12), n(10)]
+		
+		ws = new WaySlice(w, 0, 2, false)
+		wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(10), n(11), n(12), n(2), n(10)]
+		
+		ws = new WaySlice(w, 0, 4, false)
+		wn = ws.replaceNodes(newnodes)
+		assert wn.getNodes() == [n(10), n(11), n(12), n(2), n(3), n(4), n(10)]
+	}
+	
+	
+}
Index: /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/fixtures/JOSMFixture.java
===================================================================
--- /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/fixtures/JOSMFixture.java	(revision 24969)
+++ /applications/editors/josm/plugins/contourmerge/test/src/org/openstreetmap/josm/plugins/contourmerge/fixtures/JOSMFixture.java	(revision 24969)
@@ -0,0 +1,75 @@
+package org.openstreetmap.josm.plugins.contourmerge.fixtures;
+
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.text.MessageFormat;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.projection.Mercator;
+import org.openstreetmap.josm.io.OsmApi;
+import org.openstreetmap.josm.tools.I18n;
+
+public class JOSMFixture {
+    static private final Logger logger = Logger.getLogger(JOSMFixture.class.getName());
+
+    static public JOSMFixture createUnitTestFixture() {
+        return new JOSMFixture("/test-unit-env.properties");
+    }
+
+    private Properties testProperties;
+    private String testPropertiesResourceName;
+
+    public JOSMFixture(String testPropertiesResourceName) {
+        this.testPropertiesResourceName = testPropertiesResourceName;
+    }
+
+    public void init() {
+        testProperties = new Properties();
+     
+        // load properties
+        //
+        try {
+            testProperties.load(JOSMFixture.class.getResourceAsStream(testPropertiesResourceName));
+        } catch(Exception e){
+            logger.log(Level.SEVERE, MessageFormat.format("failed to load property file ''{0}''", testPropertiesResourceName));
+            fail(MessageFormat.format("failed to load property file ''{0}''. \nMake sure the path ''$project_root/test/config'' is on the classpath.", testPropertiesResourceName));
+        }
+
+        // check josm.home
+        //
+        String josmHome = testProperties.getProperty("josm.home");
+        if (josmHome == null) {
+            fail(MessageFormat.format("property ''{0}'' not set in test environment", "josm.home"));
+        } else {
+            File f = new File(josmHome);
+            if (! f.exists() || ! f.canRead()) {
+                fail(MessageFormat.format("property ''{0}'' points to ''{1}'' which is either not existing or not readable.\nEdit ''{2}'' and update the value ''josm.home''. ", "josm.home", josmHome,testPropertiesResourceName ));
+            }
+        }
+        System.setProperty("josm.home", josmHome);
+        Main.pref = new Preferences();
+        I18n.init();
+        // initialize the plaform hook, and
+        Main.determinePlatformHook();
+        // call the really early hook before we anything else
+        Main.platform.preStartupHook();
+
+        Main.pref.init(false);
+
+        // init projection
+        Main.proj = new Mercator();
+
+        // make sure we don't upload to or test against production
+        //
+        String url = OsmApi.getOsmApi().getBaseUrl().toLowerCase().trim();
+        if (url.startsWith("http://www.openstreetmap.org")
+                || url.startsWith("http://api.openstreetmap.org")) {
+            fail(MessageFormat.format("configured server url ''{0}'' seems to be a productive url, aborting.", url));
+        }
+    }
+}
