Index: applications/editors/josm/plugins/turnlanes/LICENSE
===================================================================
--- applications/editors/josm/plugins/turnlanes/LICENSE	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/LICENSE	(revision 25606)
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, 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 Lesser 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 Street, 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 Lesser General
+Public License instead of this License.
Index: applications/editors/josm/plugins/turnlanes/README
===================================================================
--- applications/editors/josm/plugins/turnlanes/README	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/README	(revision 25606)
@@ -0,0 +1,7 @@
+README 
+======
+
+turnlanes is a JOSM plugin for editing and validating junction information in
+OpenStreetMap. The source code is licensed under GPL v2 or later. The original
+author is Benjamin Schulz.
+
Index: applications/editors/josm/plugins/turnlanes/build.xml
===================================================================
--- applications/editors/josm/plugins/turnlanes/build.xml	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/build.xml	(revision 25606)
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+** This is a template build file for a JOSM  plugin.
+**
+** Maintaining versions
+** ====================
+** see README.template
+**
+** Usage
+** =====
+** To build it run
+**
+**    > ant  dist
+**
+** To install the generated plugin locally (in you default plugin directory) run
+**
+**    > ant  install
+**
+** The generated plugin jar is not automatically available in JOSMs plugin configuration
+** dialog. You have to check it in first.
+**
+** Use the ant target 'publish' to check in the plugin and make it available to other
+** JOSM users:
+**    set the properties commit.message and plugin.main.version
+** and run
+**    > ant  publish
+**
+**
+-->
+<project name="turnlanes" default="dist" basedir=".">
+
+	<!-- enter the SVN commit message -->
+	<property name="commit.message" value="Commit message" />
+	<!-- enter the *lowest* JOSM version this plugin is currently compatible with -->
+	<property name="plugin.main.version" value="3518" />
+
+
+	<!--
+      ************************************************
+      ** 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 ${ant.project.name}.jar ... " />
+		<copy todir="${plugin.build.dir}/resources">
+			<fileset dir="resources" />
+		</copy>
+		<copy todir="${plugin.build.dir}/images">
+			<fileset dir="images" />
+		</copy>
+		<copy todir="${plugin.build.dir}">
+			<fileset dir=".">
+				<include name="README" />
+				<include name="LICENSE" />
+			</fileset>
+		</copy>
+		<jar destfile="${plugin.jar}" basedir="${plugin.build.dir}">
+			<!--
+        ************************************************
+        ** configure these properties. Most of them will be copied to the plugins
+        ** manifest file. Property values will also show up in the list available
+        ** plugins: http://josm.openstreetmap.de/wiki/Plugins.
+        **
+        ************************************************
+    -->
+			<manifest>
+				<attribute name="Author" value="Benjamin Schulz" />
+				<attribute name="Plugin-Class" value="org.openstreetmap.josm.plugins.turnlanes.TurnLanesPlugin" />
+				<attribute name="Plugin-Date" value="2011-03-17" />
+				<attribute name="Plugin-Description" value="Provides a straightforward GUI for adding, editing and deleting turn lanes." />
+				<!--<attribute name="Plugin-Icon" value="..." />-->
+				<!--<attribute name="Plugin-Link" value="..." />-->
+				<attribute name="Plugin-Mainversion" value="${plugin.main.version}" />
+				<attribute name="Plugin-Version" value="1" />
+			</manifest>
+		</jar>
+	</target>
+
+	<!--
+    **********************************************************
+    ** revision - extracts the current revision number for the
+    **    file build.number and stores it in the XML property
+    **    version.*
+    **********************************************************
+    -->
+	<target name="revision">
+
+		<exec append="false" output="REVISION" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="info" />
+			<arg value="--xml" />
+			<arg value="." />
+		</exec>
+		<xmlproperty file="REVISION" prefix="version" keepRoot="false" collapseAttributes="true" />
+		<delete file="REVISION" />
+	</target>
+
+	<!--
+    **********************************************************
+    ** clean - clean up the build environment
+    **********************************************************
+    -->
+	<target name="clean">
+		<delete dir="${plugin.build.dir}" />
+		<delete file="${plugin.jar}" />
+	</target>
+
+	<!--
+    **********************************************************
+    ** install - install the plugin in your local JOSM installation
+    **********************************************************
+    -->
+	<target name="install" depends="dist">
+		<property environment="env" />
+		<condition property="josm.plugins.dir" value="${env.APPDATA}/JOSM/plugins" else="${user.home}/.josm/plugins">
+			<and>
+				<os family="windows" />
+			</and>
+		</condition>
+		<copy file="${plugin.jar}" todir="${josm.plugins.dir}" />
+	</target>
+
+
+	<!--
+    ************************** Publishing the plugin *********************************** 
+    -->
+	<!--
+        ** extracts the JOSM release for the JOSM version in ../core and saves it in the 
+        ** property ${coreversion.info.entry.revision}
+        **
+        -->
+	<target name="core-info">
+		<exec append="false" output="core.info.xml" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="info" />
+			<arg value="--xml" />
+			<arg value="../../core" />
+		</exec>
+		<xmlproperty file="core.info.xml" prefix="coreversion" keepRoot="true" collapseAttributes="true" />
+		<echo>Building against core revision ${coreversion.info.entry.revision}.</echo>
+		<echo>Plugin-Mainversion is set to ${plugin.main.version}.</echo>
+		<delete file="core.info.xml" />
+	</target>
+
+	<!--
+        ** commits the source tree for this plugin
+        -->
+	<target name="commit-current">
+		<echo>Commiting the plugin source with message '${commit.message}' ...</echo>
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="commit" />
+			<arg value="-m '${commit.message}'" />
+			<arg value="." />
+		</exec>
+	</target>
+
+	<!--
+        ** updates (svn up) the source tree for this plugin
+        -->
+	<target name="update-current">
+		<echo>Updating plugin source ...</echo>
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="up" />
+			<arg value="." />
+		</exec>
+		<echo>Updating ${plugin.jar} ...</echo>
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="up" />
+			<arg value="../dist/${plugin.jar}" />
+		</exec>
+	</target>
+
+	<!--
+        ** commits the plugin.jar 
+        -->
+	<target name="commit-dist">
+		<echo>
+    ***** Properties of published ${plugin.jar} *****
+    Commit message    : '${commit.message}'                    
+    Plugin-Mainversion: ${plugin.main.version}
+    JOSM build version: ${coreversion.info.entry.revision}
+    Plugin-Version    : ${version.entry.commit.revision}
+    ***** / Properties of published ${plugin.jar} *****                    
+                        
+    Now commiting ${plugin.jar} ...
+    </echo>
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+			<env key="LANG" value="C" />
+			<arg value="-m '${commit.message}'" />
+			<arg value="commit" />
+			<arg value="${plugin.jar}" />
+		</exec>
+	</target>
+
+	<!-- ** make sure svn is present as a command line tool ** -->
+	<target name="ensure-svn-present">
+		<exec append="true" output="svn.log" executable="svn" failifexecutionfails="false" failonerror="false" resultproperty="svn.exit.code">
+			<env key="LANG" value="C" />
+			<arg value="--version" />
+		</exec>
+		<fail message="Fatal: command 'svn --version' failed. Please make sure svn is installed on your system.">
+			<!-- return code not set at all? Most likely svn isn't installed -->
+			<condition>
+				<not>
+					<isset property="svn.exit.code" />
+				</not>
+			</condition>
+		</fail>
+		<fail message="Fatal: command 'svn --version' failed. Please make sure a working copy of svn is installed on your system.">
+			<!-- error code from SVN? Most likely svn is not what we are looking on this system -->
+			<condition>
+				<isfailure code="${svn.exit.code}" />
+			</condition>
+		</fail>
+	</target>
+
+	<target name="publish" depends="ensure-svn-present,core-info,commit-current,update-current,clean,dist,commit-dist">
+	</target>
+</project>
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/CollectionUtils.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/CollectionUtils.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/CollectionUtils.java	(revision 25606)
@@ -0,0 +1,33 @@
+package org.openstreetmap.josm.plugins.turnlanes;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+public class CollectionUtils {
+	public static <E> Iterable<E> reverse(final List<E> list) {
+		return new Iterable<E>() {
+			@Override
+			public Iterator<E> iterator() {
+				final ListIterator<E> it = list.listIterator(list.size());
+				
+				return new Iterator<E>() {
+					@Override
+					public boolean hasNext() {
+						return it.hasPrevious();
+					}
+					
+					@Override
+					public E next() {
+						return it.previous();
+					}
+					
+					@Override
+					public void remove() {
+						it.remove();
+					}
+				};
+			}
+		};
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/TurnLanesPlugin.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/TurnLanesPlugin.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/TurnLanesPlugin.java	(revision 25606)
@@ -0,0 +1,20 @@
+package org.openstreetmap.josm.plugins.turnlanes;
+
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.plugins.turnlanes.gui.TurnLanesDialog;
+
+public class TurnLanesPlugin extends Plugin {
+	public TurnLanesPlugin(PluginInformation info) {
+		super(info);
+	}
+	
+	@Override
+	public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+		if (oldFrame == null && newFrame != null) {
+			// there was none before
+			newFrame.addToggleDialog(new TurnLanesDialog());
+		}
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/GuiContainer.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/GuiContainer.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/GuiContainer.java	(revision 25606)
@@ -0,0 +1,110 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Stroke;
+import java.awt.geom.Point2D;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openstreetmap.josm.plugins.turnlanes.model.Junction;
+import org.openstreetmap.josm.plugins.turnlanes.model.Lane;
+import org.openstreetmap.josm.plugins.turnlanes.model.Road;
+
+class GuiContainer {
+	static final Color RED = new Color(234, 66, 108);
+	static final Color GREEN = new Color(66, 234, 108);
+	
+	private final Point2D translation;
+	/**
+	 * Meters per pixel.
+	 */
+	private final double mpp;
+	/**
+	 * Meters per source unit.
+	 */
+	private final double mpsu;
+	private final double scale;
+	private final double laneWidth;
+	
+	private final Map<Junction, JunctionGui> junctions = new HashMap<Junction, JunctionGui>();
+	private final Map<Road, RoadGui> roads = new HashMap<Road, RoadGui>();
+	
+	private final Stroke connectionStroke;
+	
+	public GuiContainer(Point2D origin, double mpsu) {
+		this.translation = new Point2D.Double(-origin.getX(), -origin.getY());
+		this.mpp = 0.2;
+		this.mpsu = mpsu;
+		this.scale = mpsu / mpp;
+		this.laneWidth = 2 / mpp;
+		
+		this.connectionStroke = new BasicStroke((float) (laneWidth / 4), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
+	}
+	
+	public JunctionGui getGui(Junction j) {
+		final JunctionGui existing = junctions.get(j);
+		if (existing != null) {
+			return existing;
+		}
+		
+		return new JunctionGui(this, j);
+	}
+	
+	void register(JunctionGui j) {
+		if (junctions.put(j.getModel(), j) != null) {
+			throw new IllegalStateException();
+		}
+	}
+	
+	public RoadGui getGui(Road r) {
+		final RoadGui gui = roads.get(r);
+		
+		if (gui == null) {
+			final RoadGui newGui = new RoadGui(this, r);
+			roads.put(r, newGui);
+			return newGui;
+		}
+		
+		return gui;
+	}
+	
+	Point2D translateAndScale(Point2D loc) {
+		return new Point2D.Double((loc.getX() + translation.getX()) * scale, (loc.getY() + translation.getY()) * scale);
+	}
+	
+	/**
+	 * @return meters per pixel
+	 */
+	public double getMpp() {
+		return mpp;
+	}
+	
+	public double getScale() {
+		return scale;
+	}
+	
+	public double getLaneWidth() {
+		return laneWidth;
+	}
+	
+	public Stroke getConnectionStroke() {
+		return connectionStroke;
+	}
+	
+	public LaneGui getGui(Lane lane) {
+		final RoadGui roadGui = roads.get(lane.getRoad());
+		
+		for (LaneGui l : roadGui.getLanes()) {
+			if (l.getModel().equals(lane)) {
+				return l;
+			}
+		}
+		
+		throw new IllegalArgumentException("No such lane.");
+	}
+	
+	public GuiContainer empty() {
+		return new GuiContainer(new Point2D.Double(-translation.getX(), -translation.getY()), mpsu);
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/GuiUtil.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/GuiUtil.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/GuiUtil.java	(revision 25606)
@@ -0,0 +1,148 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import static java.lang.Math.PI;
+import static java.lang.Math.abs;
+import static java.lang.Math.min;
+
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.Node;
+
+class GuiUtil {
+	static double normalize(double a) {
+		while (a < 0) {
+			a += 2 * Math.PI;
+		}
+		while (a > 2 * Math.PI) {
+			a -= 2 * Math.PI;
+		}
+		return a;
+	}
+	
+	// control point factor for curves (circle segment of angle a)
+	static double cpf(double a, double scale) {
+		return 4.0 / 3 * Math.tan(min(abs(a), PI - 0.001) / 4) * scale;
+	}
+	
+	static Point2D intersection(Line2D a, Line2D b) {
+		final double aa = GuiUtil.angle(a);
+		final double ab = GuiUtil.angle(b);
+		
+		// less than 1/2 degree => no intersection
+		if (Math.abs(Math.PI - abs(minAngleDiff(aa, ab))) < PI / 360) {
+			return null;
+		}
+		
+		final double d = (a.getX1() - a.getX2()) * (b.getY1() - b.getY2()) - (a.getY1() - a.getY2())
+		    * (b.getX1() - b.getX2());
+		
+		final double x = ((b.getX1() - b.getX2()) * (a.getX1() * a.getY2() - a.getY1() * a.getX2()) - (a.getX1() - a
+		    .getX2()) * (b.getX1() * b.getY2() - b.getY1() * b.getX2()))
+		    / d;
+		final double y = ((b.getY1() - b.getY2()) * (a.getX1() * a.getY2() - a.getY1() * a.getX2()) - (a.getY1() - a
+		    .getY2()) * (b.getX1() * b.getY2() - b.getY1() * b.getX2()))
+		    / d;
+		
+		return new Point2D.Double(x, y);
+	}
+	
+	static Point2D closest(Line2D l, Point2D p) {
+		final Point2D lv = vector(l.getP1(), l.getP2());
+		final double numerator = dot(vector(l.getP1(), p), lv);
+		
+		if (numerator < 0) {
+			return l.getP1();
+		}
+		
+		final double denominator = dot(lv, lv);
+		if (numerator >= denominator) {
+			return l.getP2();
+		}
+		
+		final double r = numerator / denominator;
+		return new Point2D.Double(l.getX1() + r * lv.getX(), l.getY1() + r * lv.getY());
+	}
+	
+	private static double dot(Point2D a, Point2D b) {
+		return a.getX() * b.getX() + a.getY() * b.getY();
+	}
+	
+	private static Point2D vector(Point2D from, Point2D to) {
+		return new Point2D.Double(to.getX() - from.getX(), to.getY() - from.getY());
+	}
+	
+	public static double angle(Point2D from, Point2D to) {
+		final double dx = to.getX() - from.getX();
+		final double dy = -(to.getY() - from.getY());
+		
+		return normalize(Math.atan2(dy, dx));
+	}
+	
+	public static Point2D relativePoint(Point2D p, double r, double a) {
+		return new Point2D.Double( //
+		    p.getX() + r * Math.cos(a), //
+		    p.getY() - r * Math.sin(a) //
+		);
+	}
+	
+	public static Line2D relativeLine(Line2D l, double r, double a) {
+		final double dx = r * Math.cos(a);
+		final double dy = -r * Math.sin(a);
+		
+		return new Line2D.Double( //
+		    l.getX1() + dx, //
+		    l.getY1() + dy, //
+		    l.getX2() + dx, //
+		    l.getY2() + dy //
+		);
+	}
+	
+	public static double angle(Line2D l) {
+		return angle(l.getP1(), l.getP2());
+	}
+	
+	public static double minAngleDiff(double a1, double a2) {
+		final double d = normalize(a2 - a1);
+		
+		return d > Math.PI ? -(2 * Math.PI - d) : d;
+	}
+	
+	public static final Point2D middle(Point2D a, Point2D b) {
+		return relativePoint(a, a.distance(b) / 2, angle(a, b));
+	}
+	
+	public static final Point2D middle(Line2D l) {
+		return middle(l.getP1(), l.getP2());
+	}
+	
+	public static Line2D line(Point2D p, double a) {
+		return new Line2D.Double(p, relativePoint(p, 1, a));
+	}
+	
+	public static Point2D loc(Node node) {
+		final EastNorth loc = Main.proj.latlon2eastNorth(node.getCoor());
+		return new Point2D.Double(loc.getX(), -loc.getY());
+	}
+	
+	public static List<Point2D> locs(Iterable<Node> nodes) {
+		final List<Point2D> locs = new ArrayList<Point2D>();
+		
+		for (Node n : nodes) {
+			locs.add(loc(n));
+		}
+		
+		return locs;
+	}
+	
+	static void area(Path2D area, Path inner, Path outer) {
+		area.append(inner.getIterator(), false);
+		area.append(ReversePathIterator.reverse(outer.getIterator()), true);
+		area.closePath();
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/InteractiveElement.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/InteractiveElement.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/InteractiveElement.java	(revision 25606)
@@ -0,0 +1,44 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import java.awt.Graphics2D;
+import java.awt.geom.Point2D;
+
+abstract class InteractiveElement {
+	interface Type {
+		Type INCOMING_CONNECTOR = new Type() {};
+		Type OUTGOING_CONNECTOR = new Type() {};
+		Type TURN_CONNECTION = new Type() {};
+		Type LANE_ADDER = new Type() {};
+		Type EXTENDER = new Type() {};
+	}
+	
+	public void paintBackground(Graphics2D g2d, State state) {}
+	
+	abstract void paint(Graphics2D g2d, State state);
+	
+	abstract boolean contains(Point2D p, State state);
+	
+	abstract Type getType();
+	
+	State activate(State old) {
+		return old;
+	}
+	
+	boolean beginDrag(double x, double y) {
+		return false;
+	}
+	
+	State drag(double x, double y, InteractiveElement target, State old) {
+		return old;
+	}
+	
+	State drop(double x, double y, InteractiveElement target, State old) {
+		return old;
+	}
+	
+	abstract int getZIndex();
+	
+	State click(State old) {
+		return old;
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/JunctionGui.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/JunctionGui.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/JunctionGui.java	(revision 25606)
@@ -0,0 +1,398 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import static java.lang.Math.PI;
+import static java.lang.Math.abs;
+import static java.lang.Math.max;
+import static java.lang.Math.tan;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.angle;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.closest;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.cpf;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.intersection;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.loc;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.normalize;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.relativePoint;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.openstreetmap.josm.plugins.turnlanes.model.Junction;
+import org.openstreetmap.josm.plugins.turnlanes.model.Road;
+import org.openstreetmap.josm.plugins.turnlanes.model.Turn;
+
+class JunctionGui {
+	private final class TurnConnection extends InteractiveElement {
+		private final Turn turn;
+		
+		private final Line2D line = new Line2D.Double();
+		
+		private Point2D dragBegin;
+		
+		private Point2D dragOffset = new Point2D.Double();
+		
+		public TurnConnection(Turn turn) {
+			this.turn = turn;
+		}
+		
+		@Override
+		void paint(Graphics2D g2d, State state) {
+			if (isVisible(state)) {
+				final LaneGui laneGui = getContainer().getGui(turn.getFrom());
+				final RoadGui roadGui = getContainer().getGui(turn.getTo().getRoad());
+				
+				g2d.setStroke(getContainer().getConnectionStroke());
+				
+				g2d.setColor(isRemoveDragOffset() ? GuiContainer.RED : GuiContainer.GREEN);
+				
+				line.setLine(laneGui.outgoing.getCenter(), roadGui.getConnector(turn.getTo()).getCenter());
+				line.setLine(line.getX1() + dragOffset.getX(), line.getY1() + dragOffset.getY(),
+				    line.getX2() + dragOffset.getX(), line.getY2() + dragOffset.getY());
+				g2d.draw(line);
+			}
+		}
+		
+		private boolean isVisible(State state) {
+			if (state instanceof State.OutgoingActive) {
+				return turn.getFrom().equals(((State.OutgoingActive) state).getLane().getModel());
+			} else if (state instanceof State.IncomingActive) {
+				return turn.getTo().equals(((State.IncomingActive) state).getRoadEnd());
+			}
+			
+			return false;
+		}
+		
+		@Override
+		boolean contains(Point2D p, State state) {
+			final Point2D closest = closest(line, p);
+			return p.distance(closest) <= strokeWidth() / 2;
+		}
+		
+		private double strokeWidth() {
+			final BasicStroke stroke = (BasicStroke) getContainer().getConnectionStroke();
+			return stroke.getLineWidth();
+		}
+		
+		@Override
+		Type getType() {
+			return Type.TURN_CONNECTION;
+		}
+		
+		@Override
+		int getZIndex() {
+			return 0;
+		}
+		
+		@Override
+		boolean beginDrag(double x, double y) {
+			dragBegin = new Point2D.Double(x, y);
+			dragOffset = new Point2D.Double();
+			return true;
+		}
+		
+		@Override
+		State drag(double x, double y, InteractiveElement target, State old) {
+			dragOffset = new Point2D.Double(x - dragBegin.getX(), y - dragBegin.getY());
+			return old;
+		}
+		
+		@Override
+		State drop(double x, double y, InteractiveElement target, State old) {
+			drag(x, y, target, old);
+			
+			if (isRemoveDragOffset()) {
+				getModel().removeTurn(turn);
+			}
+			
+			dragOffset = new Point2D.Double();
+			return new State.Dirty(old);
+		}
+		
+		private boolean isRemoveDragOffset() {
+			final double r = getContainer().getGui(turn.getFrom().getRoad()).connectorRadius;
+			final double max = r - strokeWidth() / 2;
+			return dragOffset.distance(0, 0) > max;
+		}
+	}
+	
+	private final class Corner {
+		final double x1;
+		final double y1;
+		
+		final double cx1;
+		final double cy1;
+		
+		final double cx2;
+		final double cy2;
+		
+		final double x2;
+		final double y2;
+		
+		public Corner(Point2D c1, Point2D cp1, Point2D cp2, Point2D c2) {
+			this.x1 = c1.getX();
+			this.y1 = c1.getY();
+			this.cx1 = cp1.getX();
+			this.cy1 = cp1.getY();
+			this.cx2 = cp2.getX();
+			this.cy2 = cp2.getY();
+			this.x2 = c2.getX();
+			this.y2 = c2.getY();
+		}
+		
+		@Override
+		public String toString() {
+			return "Corner [x1=" + x1 + ", y1=" + y1 + ", cx1=" + cx1 + ", cy1=" + cy1 + ", cx2=" + cx2 + ", cy2=" + cy2
+			    + ", x2=" + x2 + ", y2=" + y2 + "]";
+		}
+	}
+	
+	private final class Linkage implements Comparable<Linkage> {
+		final RoadGui roadGui;
+		final Road.End roadEnd;
+		final double angle;
+		
+		double lTrim;
+		double rTrim;
+		
+		public Linkage(Road.End roadEnd) {
+			this.roadGui = getContainer().getGui(roadEnd.getRoad());
+			this.roadEnd = roadEnd;
+			this.angle = normalize(roadGui.getAngle(roadEnd) + PI);
+			
+			roads.put(angle, this);
+		}
+		
+		@Override
+		public int compareTo(Linkage o) {
+			return Double.compare(angle, o.angle);
+		}
+		
+		public void trimLeft(Linkage right) {
+			right.trimRight(this);
+			
+			final Line2D leftCurb = roadGui.getLeftCurb(roadEnd);
+			final Line2D rightCurb = right.roadGui.getRightCurb(right.roadEnd);
+			
+			final double leftAngle = angle(leftCurb);
+			final double rightAngle = angle(rightCurb);
+			
+			final Point2D isect;
+			if (abs(PI - normalize(rightAngle - leftAngle)) > PI / 12) {
+				isect = intersection(leftCurb, rightCurb);
+			} else {
+				isect = GuiUtil.relativePoint(leftCurb.getP1(), roadGui.getWidth(roadEnd) / 2, angle);
+			}
+			
+			if (Math.abs(leftAngle - angle(leftCurb.getP1(), isect)) < 0.1) {
+				lTrim = leftCurb.getP1().distance(isect);
+			}
+		}
+		
+		private void trimRight(Linkage left) {
+			final Line2D rightCurb = roadGui.getRightCurb(roadEnd);
+			final Line2D leftCurb = left.roadGui.getLeftCurb(left.roadEnd);
+			
+			final double rightAngle = angle(rightCurb);
+			final double leftAngle = angle(leftCurb);
+			
+			final Point2D isect;
+			if (abs(PI - normalize(rightAngle - leftAngle)) > PI / 12) {
+				isect = intersection(rightCurb, leftCurb);
+			} else {
+				isect = GuiUtil.relativePoint(rightCurb.getP1(), roadGui.getWidth(roadEnd) / 2, angle);
+			}
+			
+			if (Math.abs(rightAngle - angle(rightCurb.getP1(), isect)) < 0.1) {
+				rTrim = rightCurb.getP1().distance(isect);
+			}
+		}
+		
+		public void trimAdjust() {
+			final double MAX_TAN = tan(PI / 2 - MAX_ANGLE);
+			
+			final double sin = roadGui.getWidth(roadEnd);
+			final double cos = abs(lTrim - rTrim);
+			final double tan = sin / cos;
+			
+			if (tan < MAX_TAN) {
+				lTrim = max(lTrim, rTrim - sin / MAX_TAN);
+				rTrim = max(rTrim, lTrim - sin / MAX_TAN);
+			}
+			
+			lTrim += container.getLaneWidth() / 2;
+			rTrim += container.getLaneWidth() / 2;
+		}
+	}
+	
+	// max angle between corners
+	private static final double MAX_ANGLE = Math.toRadians(30);
+	
+	private final GuiContainer container;
+	private final Junction junction;
+	
+	final double x;
+	final double y;
+	
+	private final NavigableMap<Double, Linkage> roads = new TreeMap<Double, Linkage>();
+	
+	private final Path2D area = new Path2D.Double();
+	
+	public JunctionGui(GuiContainer container, Junction j) {
+		this.container = container;
+		this.junction = j;
+		
+		container.register(this);
+		
+		final Point2D loc = container.translateAndScale(loc(j.getNode()));
+		this.x = loc.getX();
+		this.y = loc.getY();
+		
+		final Set<Road> done = new HashSet<Road>();
+		for (Road r : j.getRoads()) {
+			if (!done.contains(r)) {
+				done.add(r);
+				
+				if (r.getFromEnd().getJunction().equals(j)) {
+					new Linkage(r.getFromEnd());
+				}
+				if (r.getToEnd().getJunction().equals(j)) {
+					new Linkage(r.getToEnd());
+				}
+			}
+		}
+		
+		recalculate();
+	}
+	
+	void recalculate() {
+		for (Linkage l : roads.values()) {
+			l.lTrim = 0;
+			l.rTrim = 0;
+		}
+		
+		area.reset();
+		if (roads.size() < 2) {
+			return;
+		}
+		
+		Linkage last = roads.lastEntry().getValue();
+		for (Linkage l : roads.values()) {
+			l.trimLeft(last);
+			last = l;
+		}
+		for (Linkage l : roads.values()) {
+			l.trimAdjust();
+		}
+		
+		boolean first = true;
+		for (Corner c : corners()) {
+			if (first) {
+				area.moveTo(c.x1, c.y1);
+				first = false;
+			} else {
+				area.lineTo(c.x1, c.y1);
+			}
+			
+			area.curveTo(c.cx1, c.cy1, c.cx2, c.cy2, c.x2, c.y2);
+		}
+		
+		area.closePath();
+	}
+	
+	private Iterable<Corner> corners() {
+		final List<Corner> result = new ArrayList<JunctionGui.Corner>(roads.size());
+		
+		Linkage last = roads.lastEntry().getValue();
+		for (Linkage l : roads.values()) {
+			result.add(corner(last, l));
+			last = l;
+		}
+		
+		return result;
+	}
+	
+	private Corner corner(Linkage right, Linkage left) {
+		final Line2D rightCurb = right.roadGui.getRightCurb(right.roadEnd);
+		final Line2D leftCurb = left.roadGui.getLeftCurb(left.roadEnd);
+		
+		final double rightAngle = angle(rightCurb);
+		final double leftAngle = angle(leftCurb);
+		
+		final double delta = normalize(leftAngle - rightAngle);
+		
+		final boolean wide = delta > PI;
+		final double a = wide ? max(0, delta - (PI + 2 * MAX_ANGLE)) : delta;
+		
+		final double cpf1 = cpf(a, container.getLaneWidth() / 2 + (wide ? right.roadGui.getWidth(right.roadEnd) : 0));
+		final double cpf2 = cpf(a, container.getLaneWidth() / 2 + (wide ? left.roadGui.getWidth(left.roadEnd) : 0));
+		
+		final Point2D c1 = relativePoint(rightCurb.getP1(), cpf1, right.angle + PI);
+		final Point2D c2 = relativePoint(leftCurb.getP1(), cpf2, left.angle + PI);
+		
+		return new Corner(rightCurb.getP1(), c1, c2, leftCurb.getP1());
+	}
+	
+	public Set<RoadGui> getRoads() {
+		final Set<RoadGui> result = new HashSet<RoadGui>();
+		
+		for (Linkage l : roads.values()) {
+			result.add(l.roadGui);
+		}
+		
+		return Collections.unmodifiableSet(result);
+	}
+	
+	double getLeftTrim(Road.End end) {
+		return getLinkage(end).lTrim;
+	}
+	
+	private Linkage getLinkage(Road.End end) {
+		final double a = normalize(getContainer().getGui(end.getRoad()).getAngle(end) + PI);
+		final Map.Entry<Double, Linkage> e = roads.floorEntry(a);
+		return e != null ? e.getValue() : null;
+	}
+	
+	double getRightTrim(Road.End end) {
+		return getLinkage(end).rTrim;
+	}
+	
+	Point2D getPoint() {
+		return new Point2D.Double(x, y);
+	}
+	
+	public GuiContainer getContainer() {
+		return container;
+	}
+	
+	public Junction getModel() {
+		return junction;
+	}
+	
+	public List<InteractiveElement> paint(Graphics2D g2d) {
+		g2d.setColor(new Color(96, 96, 96));
+		g2d.fill(area);
+		
+		final List<InteractiveElement> result = new ArrayList<InteractiveElement>();
+		
+		for (Turn t : getModel().getTurns()) {
+			result.add(new TurnConnection(t));
+		}
+		
+		return result;
+	}
+	
+	public Rectangle2D getBounds() {
+		return area.getBounds2D();
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/JunctionPane.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/JunctionPane.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/JunctionPane.java	(revision 25606)
@@ -0,0 +1,334 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseWheelEvent;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import javax.swing.JComponent;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.plugins.turnlanes.model.ModelContainer;
+
+class JunctionPane extends JComponent {
+	private final class MouseInputProcessor extends MouseAdapter {
+		private int originX;
+		private int originY;
+		private int button;
+		
+		public void mousePressed(MouseEvent e) {
+			button = e.getButton();
+			
+			if (button == MouseEvent.BUTTON1) {
+				final Point2D mouse = translateMouseCoords(e);
+				for (InteractiveElement ie : interactives()) {
+					if (ie.contains(mouse, state)) {
+						setState(ie.activate(state));
+						repaint();
+						break;
+					}
+				}
+			}
+			
+			originX = e.getX();
+			originY = e.getY();
+		}
+		
+		@Override
+		public void mouseReleased(MouseEvent e) {
+			if (dragging != null) {
+				final Point2D mouse = translateMouseCoords(e);
+				setState(dragging.drop(mouse.getX(), mouse.getY(), dropTarget(mouse), state));
+			}
+			
+			dragging = null;
+			repaint();
+		}
+		
+		private InteractiveElement dropTarget(Point2D mouse) {
+			for (InteractiveElement ie : interactives()) {
+				if (ie.contains(mouse, state)) {
+					return ie;
+				}
+			}
+			
+			return null;
+		}
+		
+		@Override
+		public void mouseClicked(MouseEvent e) {
+			if (button == MouseEvent.BUTTON1) {
+				final Point2D mouse = translateMouseCoords(e);
+				for (InteractiveElement ie : interactives()) {
+					if (ie.contains(mouse, state)) {
+						setState(ie.click(state));
+						repaint();
+						break;
+					}
+				}
+			}
+		}
+		
+		public void mouseDragged(MouseEvent e) {
+			if (button == MouseEvent.BUTTON1) {
+				final Point2D mouse = translateMouseCoords(e);
+				
+				if (dragging == null) {
+					final Point2D origin = translateCoords(originX, originY);
+					for (InteractiveElement ie : interactives()) {
+						if (ie.contains(origin, state)) {
+							if (ie.beginDrag(origin.getX(), origin.getY())) {
+								dragging = ie;
+							}
+							
+							break;
+						}
+					}
+				}
+				
+				if (dragging != null) {
+					setState(dragging.drag(mouse.getX(), mouse.getY(), dropTarget(mouse), state));
+				}
+				
+				repaint();
+			} else if (button == MouseEvent.BUTTON3) {
+				translate(e.getX() - originX, e.getY() - originY);
+				
+				originX = e.getX();
+				originY = e.getY();
+			}
+		}
+		
+		@Override
+		public void mouseWheelMoved(MouseWheelEvent e) {
+			scale(e.getX(), e.getY(), Math.pow(0.8, e.getWheelRotation()));
+		}
+		
+		private Point2D translateMouseCoords(MouseEvent e) {
+			return translateCoords(e.getX(), e.getY());
+		}
+		
+		private Point2D translateCoords(int x, int y) {
+			return new Point2D.Double(-translationX + x / scale, -translationY + y / scale);
+		}
+	}
+	
+	private static final long serialVersionUID = 6917061040674799271L;
+	
+	private static final Color TRANSPARENT = new Color(0, 0, 0, 0);
+	
+	private final MouseInputProcessor mip = new MouseInputProcessor();
+	
+	private JunctionGui junction;
+	
+	private int width = 0;
+	private int height = 0;
+	private double scale = 10;
+	private double translationX = 0;
+	private double translationY = 0;
+	private boolean dirty = true;
+	private BufferedImage passive;
+	private BufferedImage interactive;
+	
+	private final NavigableMap<Integer, List<InteractiveElement>> interactives = new TreeMap<Integer, List<InteractiveElement>>();
+	private State state;
+	private InteractiveElement dragging;
+	
+	public JunctionPane(JunctionGui junction) {
+		setJunction(junction);
+	}
+	
+	public void setJunction(JunctionGui junction) {
+		removeMouseListener(mip);
+		removeMouseMotionListener(mip);
+		removeMouseWheelListener(mip);
+		interactives.clear();
+		dragging = null;
+		this.junction = junction;
+		
+		dirty = true;
+		repaint();
+		
+		if (junction == null) {
+			this.state = null;
+		} else {
+			this.state = new State.Default(junction);
+			
+			final Rectangle2D bounds = junction.getBounds();
+			scale = Math.min(getHeight() / 2 / bounds.getHeight(), getWidth() / 2 / bounds.getWidth());
+			
+			translationX = -bounds.getCenterX();
+			translationY = -bounds.getCenterY();
+			
+			translate(getWidth() / 2d, getHeight() / 2d);
+			
+			addMouseListener(mip);
+			addMouseMotionListener(mip);
+			addMouseWheelListener(mip);
+		}
+	}
+	
+	private void setState(State state) {
+		if (state instanceof State.Invalid) {
+			final Node n = junction.getModel().getNode();
+			final ModelContainer m = ModelContainer.create(n);
+			
+			final GuiContainer c = junction.getContainer().empty();
+			junction = c.getGui(m.getJunction(n));
+			
+			dirty = true;
+			this.state = new State.Default(junction);
+		} else if (state instanceof State.Dirty) {
+			dirty = true;
+			this.state = ((State.Dirty) state).unwrap();
+		} else {
+			this.state = state;
+		}
+	}
+	
+	void scale(int x, int y, double scale) {
+		this.scale *= scale;
+		
+		final double w = getWidth();
+		final double h = getHeight();
+		
+		translationX -= (w * (scale - 1)) / (2 * this.scale);
+		translationY -= (h * (scale - 1)) / (2 * this.scale);
+		
+		dirty = true;
+		repaint();
+	}
+	
+	void translate(double x, double y) {
+		translationX += x / scale;
+		translationY += y / scale;
+		
+		dirty = true;
+		repaint();
+	}
+	
+	@Override
+	protected void paintComponent(Graphics g) {
+		if (getWidth() != width || getHeight() != height) {
+			translate((getWidth() - width) / 2d, (getHeight() - height) / 2d);
+			width = getWidth();
+			height = getHeight();
+			
+			// translate already set dirty flag
+			passive = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
+			interactive = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+		}
+		
+		if (junction == null) {
+			super.paintComponent(g);
+			return;
+		}
+		
+		if (dirty) {
+			paintPassive((Graphics2D) passive.getGraphics());
+			dirty = false;
+		}
+		paintInteractive((Graphics2D) interactive.getGraphics());
+		
+		final Graphics2D g2d = (Graphics2D) g;
+		
+		g2d.drawImage(passive, 0, 0, getWidth(), getHeight(), null);
+		g2d.drawImage(interactive, 0, 0, getWidth(), getHeight(), null);
+	}
+	
+	private void paintInteractive(Graphics2D g2d) {
+		g2d.setBackground(TRANSPARENT);
+		g2d.clearRect(0, 0, getWidth(), getHeight());
+		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+		g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+		g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
+		
+		g2d.scale(scale, scale);
+		
+		g2d.translate(translationX, translationY);
+		
+		g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.7f));
+		
+		for (Map.Entry<Integer, List<InteractiveElement>> e : interactives.entrySet()) {
+			for (InteractiveElement ie : e.getValue()) {
+				ie.paintBackground(g2d, state);
+			}
+			for (InteractiveElement ie : e.getValue()) {
+				ie.paint(g2d, state);
+			}
+		}
+	}
+	
+	private List<InteractiveElement> interactives() {
+		final List<InteractiveElement> result = new ArrayList<InteractiveElement>();
+		
+		for (List<InteractiveElement> ies : interactives.descendingMap().values()) {
+			result.addAll(ies);
+		}
+		
+		return result;
+	}
+	
+	private void paintPassive(Graphics2D g2d) {
+		interactives.clear();
+		
+		g2d.setBackground(new Color(100, 160, 240));
+		g2d.clearRect(0, 0, getWidth(), getHeight());
+		
+		g2d.scale(scale, scale);
+		g2d.translate(translationX, translationY);
+		
+		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+		
+		g2d.setColor(Color.GRAY);
+		for (RoadGui r : junction.getRoads()) {
+			addAllInteractives(r.paint(g2d));
+		}
+		
+		addAllInteractives(junction.paint(g2d));
+		
+		dot(g2d, new Point2D.Double(junction.x, junction.y), junction.getContainer().getLaneWidth() / 5);
+	}
+	
+	private void addAllInteractives(List<InteractiveElement> ies) {
+		for (InteractiveElement ie : ies) {
+			final List<InteractiveElement> existing = interactives.get(ie.getZIndex());
+			
+			final List<InteractiveElement> list;
+			if (existing == null) {
+				list = new ArrayList<InteractiveElement>();
+				interactives.put(ie.getZIndex(), list);
+			} else {
+				list = existing;
+			}
+			
+			list.add(ie);
+		}
+	}
+	
+	static void dot(Graphics2D g2d, Point2D p, double r, Color c) {
+		final Color old = g2d.getColor();
+		
+		g2d.setColor(c);
+		g2d.fill(new Ellipse2D.Double(p.getX() - r, p.getY() - r, 2 * r, 2 * r));
+		
+		g2d.setColor(old);
+	}
+	
+	static void dot(Graphics2D g2d, Point2D p, double r) {
+		dot(g2d, p, r, Color.RED);
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/LaneGui.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/LaneGui.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/LaneGui.java	(revision 25606)
@@ -0,0 +1,314 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import static java.lang.Math.max;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.area;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.List;
+
+import org.openstreetmap.josm.plugins.turnlanes.gui.RoadGui.IncomingConnector;
+import org.openstreetmap.josm.plugins.turnlanes.model.Junction;
+import org.openstreetmap.josm.plugins.turnlanes.model.Lane;
+
+final class LaneGui {
+	final class LengthSlider extends InteractiveElement {
+		private final Point2D center = new Point2D.Double();
+		private final Ellipse2D circle = new Ellipse2D.Double();
+		
+		private Point2D dragDelta;
+		
+		private LengthSlider() {}
+		
+		@Override
+		public void paint(Graphics2D g2d, State state) {
+			if (isVisible(state)) {
+				g2d.setColor(Color.BLUE);
+				g2d.fill(circle);
+				
+				final String len = METER_FORMAT.format(getModel().getLength());
+				final Rectangle2D bounds = circle.getBounds2D();
+				g2d.setFont(g2d.getFont().deriveFont(Font.BOLD, (float) bounds.getHeight()));
+				g2d.drawString(len, (float) bounds.getMaxX(), (float) bounds.getMaxY());
+			}
+		}
+		
+		private boolean isVisible(State state) {
+			if (state instanceof State.OutgoingActive) {
+				return LaneGui.this.equals(((State.OutgoingActive) state).getLane());
+			}
+			
+			return false;
+		}
+		
+		@Override
+		public boolean contains(Point2D p, State state) {
+			return isVisible(state) && circle.contains(p);
+		}
+		
+		@Override
+		public Type getType() {
+			return Type.INCOMING_CONNECTOR;
+		}
+		
+		@Override
+		boolean beginDrag(double x, double y) {
+			dragDelta = new Point2D.Double(center.getX() - x, center.getY() - y);
+			return true;
+		}
+		
+		@Override
+		State drag(double x, double y, InteractiveElement target, State old) {
+			move(x + dragDelta.getX(), y + dragDelta.getY());
+			return new State.Dirty(old);
+		}
+		
+		void move(double x, double y) {
+			final double r = getRoad().connectorRadius;
+			
+			final double offset = getRoad().getOffset(x, y);
+			final double newLength = getModel().isReverse() ? offset : getRoad().getLength() - offset;
+			if (newLength > 0) {
+				getModel().setLength(newLength * getRoad().getContainer().getMpp());
+			}
+			
+			center.setLocation(x, y);
+			circle.setFrame(x - r, y - r, 2 * r, 2 * r);
+		}
+		
+		public void move(Point2D loc) {
+			final double x = loc.getX();
+			final double y = loc.getY();
+			final double r = getRoad().connectorRadius;
+			
+			center.setLocation(x, y);
+			circle.setFrame(x - r, y - r, 2 * r, 2 * r);
+		}
+		
+		@Override
+		int getZIndex() {
+			return 2;
+		}
+	}
+	
+	final class OutgoingConnector extends InteractiveElement {
+		private final Point2D center = new Point2D.Double();
+		private final Ellipse2D circle = new Ellipse2D.Double();
+		
+		private Point2D dragLocation;
+		private IncomingConnector dropTarget;
+		
+		private OutgoingConnector() {}
+		
+		@Override
+		public void paintBackground(Graphics2D g2d, State state) {
+			if (isActive(state)) {
+				final Composite old = g2d.getComposite();
+				g2d.setComposite(((AlphaComposite) old).derive(0.2f));
+				
+				g2d.setColor(new Color(255, 127, 31));
+				LaneGui.this.fill(g2d);
+				
+				g2d.setComposite(old);
+			}
+			
+			if (dragLocation != null) {
+				g2d.setStroke(getContainer().getConnectionStroke());
+				g2d.setColor(dropTarget == null ? GuiContainer.RED : GuiContainer.GREEN);
+				g2d.draw(new Line2D.Double(getCenter(), dropTarget == null ? dragLocation : dropTarget.getCenter()));
+			}
+		}
+		
+		@Override
+		public void paint(Graphics2D g2d, State state) {
+			if (isVisible(state)) {
+				final Composite old = g2d.getComposite();
+				if (isActive(state)) {
+					g2d.setComposite(((AlphaComposite) old).derive(1f));
+				}
+				
+				g2d.setColor(Color.WHITE);
+				g2d.fill(circle);
+				g2d.setComposite(old);
+			}
+		}
+		
+		private boolean isActive(State state) {
+			return state instanceof State.OutgoingActive && LaneGui.this.equals(((State.OutgoingActive) state).getLane());
+		}
+		
+		private boolean isVisible(State state) {
+			return getModel().getOutgoingJunction().equals(state.getJunction().getModel());
+		}
+		
+		@Override
+		public boolean contains(Point2D p, State state) {
+			return isVisible(state) && (circle.contains(p) || LaneGui.this.contains(p));
+		}
+		
+		@Override
+		public Type getType() {
+			return Type.OUTGOING_CONNECTOR;
+		}
+		
+		@Override
+		public State activate(State old) {
+			return new State.OutgoingActive(old.getJunction(), LaneGui.this);
+		}
+		
+		@Override
+		boolean beginDrag(double x, double y) {
+			return circle.contains(x, y);
+		}
+		
+		@Override
+		State drag(double x, double y, InteractiveElement target, State old) {
+			dragLocation = new Point2D.Double(x, y);
+			dropTarget = target != null && target.getType() == Type.INCOMING_CONNECTOR ? (IncomingConnector) target : null;
+			return old;
+		}
+		
+		@Override
+		State drop(double x, double y, InteractiveElement target, State old) {
+			drag(x, y, target, old);
+			dragLocation = null;
+			
+			if (dropTarget == null) {
+				return old;
+			}
+			
+			final Junction j = (getModel().isReverse() ? getRoad().getA() : getRoad().getB()).getModel();
+			
+			j.addTurn(getModel(), dropTarget.getRoadEnd());
+			
+			dropTarget = null;
+			return new State.Dirty(old);
+		}
+		
+		public Point2D getCenter() {
+			return (Point2D) center.clone();
+		}
+		
+		void move(double x, double y) {
+			final double r = getRoad().connectorRadius;
+			
+			center.setLocation(x, y);
+			circle.setFrame(x - r, y - r, 2 * r, 2 * r);
+		}
+		
+		@Override
+		int getZIndex() {
+			return 1;
+		}
+	}
+	
+	static final NumberFormat METER_FORMAT = new DecimalFormat("0.0m");
+	
+	private final RoadGui road;
+	private final Lane lane;
+	
+	final Path2D area = new Path2D.Double();
+	
+	final OutgoingConnector outgoing = new OutgoingConnector();
+	final LengthSlider lengthSlider;
+	
+	private Shape clip;
+	
+	public LaneGui(RoadGui road, Lane lane) {
+		this.road = road;
+		this.lane = lane;
+		this.lengthSlider = lane.isExtra() ? new LengthSlider() : null;
+	}
+	
+	public double getLength() {
+		return getModel().isExtra() ? lane.getLength() / getRoad().getContainer().getMpp() : getRoad().getLength();
+	}
+	
+	public Lane getModel() {
+		return lane;
+	}
+	
+	public RoadGui getRoad() {
+		return road;
+	}
+	
+	public GuiContainer getContainer() {
+		return getRoad().getContainer();
+	}
+	
+	public Path recalculate(Path inner, Path2D innerLine) {
+		area.reset();
+		
+		final double W = getContainer().getLaneWidth();
+		final double L = getLength();
+		
+		final double WW = 3 / getContainer().getMpp();
+		
+		final List<LaneGui> lanes = getRoad().getLanes();
+		final int i = lanes.indexOf(this);
+		final LaneGui left = getModel().isReverse() ? (i < lanes.size() - 1 ? lanes.get(i + 1) : null) : (i > 0 ? lanes
+		    .get(i - 1) : null);
+		final Lane leftModel = left == null ? null : left.getModel();
+		final double leftLength = leftModel == null || leftModel.isReverse() != getModel().isReverse() ? Double.NEGATIVE_INFINITY
+		    : leftModel.getKind() == Lane.Kind.EXTRA_LEFT ? left.getLength() : L;
+		
+		final Path outer;
+		if (getModel().getKind() == Lane.Kind.EXTRA_LEFT) {
+			final double AL = 30 / getContainer().getMpp();
+			final double SL = max(L, leftLength + AL);
+			
+			outer = inner.offset(W, SL, SL + AL, 0);
+			area(area, inner.subpath(0, L), outer.subpath(0, L + WW));
+			
+			lengthSlider.move(inner.getPoint(L));
+			
+			if (L > leftLength) {
+				innerLine.append(inner.subpath(max(0, leftLength + WW), L).getIterator(), leftLength >= 0
+				    || getModel().isReverse());
+				final Point2D op = outer.getPoint(L + WW);
+				innerLine.lineTo(op.getX(), op.getY());
+			}
+		} else if (getModel().getKind() == Lane.Kind.EXTRA_RIGHT) {
+			outer = inner.offset(W, L, L + WW, 0);
+			area(area, inner.subpath(0, L + WW), outer.subpath(0, L));
+			
+			lengthSlider.move(outer.getPoint(L));
+		} else {
+			outer = inner.offset(W, -1, -1, W);
+			area(area, inner, outer);
+			
+			if (leftLength < L) {
+				innerLine.append(inner.subpath(max(0, leftLength + WW), L).getIterator(), leftLength >= 0
+				    || getModel().isReverse());
+			}
+		}
+		
+		return outer;
+	}
+	
+	public void fill(Graphics2D g2d) {
+		final Shape old = g2d.getClip();
+		g2d.clip(clip);
+		g2d.fill(area);
+		g2d.setClip(old);
+	}
+	
+	public void setClip(Shape clip) {
+		this.clip = clip;
+	}
+	
+	public boolean contains(Point2D p) {
+		return area.contains(p) && clip.contains(p);
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/Path.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/Path.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/Path.java	(revision 25606)
@@ -0,0 +1,577 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import static java.lang.Math.PI;
+import static java.lang.Math.abs;
+import static java.lang.Math.cos;
+import static java.lang.Math.hypot;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+import static java.lang.Math.signum;
+import static java.lang.Math.sin;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.angle;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.cpf;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.minAngleDiff;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.relativePoint;
+
+import java.awt.geom.Line2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.util.NoSuchElementException;
+
+/**
+ * A path that allows constructing offset curves/parallel curves with a somewhat crude straight
+ * skeleton implementation.
+ * 
+ * @author Ben Schulz
+ */
+abstract class Path {
+	private static final class SimplePathIterator implements PathIterator {
+		private final SimplePathIterator previous;
+		
+		private final int type;
+		private final double[] coords;
+		
+		private boolean done = false;
+		
+		public SimplePathIterator(SimplePathIterator previous, int type, double... coords) {
+			this.previous = previous;
+			this.type = type;
+			this.coords = coords;
+		}
+		
+		public SimplePathIterator(int type, double... coords) {
+			this(null, type, coords);
+		}
+		
+		@Override
+		public int getWindingRule() {
+			return WIND_NON_ZERO;
+		}
+		
+		@Override
+		public boolean isDone() {
+			return done;
+		}
+		
+		@Override
+		public void next() {
+			if (previous != null && !previous.isDone()) {
+				previous.next();
+			} else {
+				done = true;
+			}
+		}
+		
+		@Override
+		public int currentSegment(float[] coords) {
+			if (previous != null && !previous.isDone()) {
+				return previous.currentSegment(coords);
+			} else if (done) {
+				throw new NoSuchElementException("Iterator is already done.");
+			}
+			
+			for (int i = 0; i < 6; ++i) {
+				coords[i] = (float) this.coords[i];
+			}
+			
+			return type;
+		}
+		
+		@Override
+		public int currentSegment(double[] coords) {
+			if (previous != null && !previous.isDone()) {
+				return previous.currentSegment(coords);
+			} else if (done) {
+				throw new NoSuchElementException("Iterator is already done.");
+			}
+			
+			for (int i = 0; i < 6; ++i) {
+				coords[i] = this.coords[i];
+			}
+			
+			return type;
+		}
+		
+	}
+	
+	private static final class Line extends Path {
+		private final Path previous;
+		
+		private final double endX;
+		private final double endY;
+		
+		private final double angle;
+		
+		private final double length;
+		
+		public Line(Path previous, double x, double y, double length) {
+			this.previous = previous;
+			
+			this.endX = x;
+			this.endY = y;
+			
+			this.angle = angle(previous.getEnd(), getEnd());
+			
+			this.length = length;
+		}
+		
+		@Override
+		public Point2D getStart() {
+			return previous.getStart();
+		}
+		
+		@Override
+		public Point2D getEnd() {
+			return new Point2D.Double(endX, endY);
+		}
+		
+		@Override
+		public double getEndAngle() {
+			return angle;
+		}
+		
+		@Override
+		public double getLength() {
+			return previous.getLength() + length;
+		}
+		
+		@Override
+		public Path offset(double ws, double m1, double m2, double we) {
+			return offsetInternal(ws, m1, m2, we, angle);
+		}
+		
+		@Override
+		Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) {
+			final double PL = previous.getLength();
+			final double ML = PL + length;
+			
+			final Path prev = previous.offsetInternal(ws, m1, m2, we, angle);
+			
+			final double wStart = PL <= m1 ? ws : m2 <= PL ? we : ws + (PL - m1) * (we - ws) / (m2 - m1);
+			final Point2D from = prev.getEnd();
+			final Point2D to = offsetEnd(wStart, endAngle);
+			
+			if (abs(minAngleDiff(angle, angle(from, to))) > PI / 100) {
+				return previous.offsetInternal(ws, m1, m2, we, endAngle);
+			}
+			
+			if (ML <= m1) {
+				return simpleOffset(prev, ws, endAngle);
+			} else if (m2 <= PL) {
+				return simpleOffset(prev, we, endAngle);
+			}
+			
+			final double LL = from.distance(to);
+			
+			final Point2D m1o = PL <= m1 ? relativePoint(prev.getEnd(), LL * (m1 - PL) / length, angle) : null;
+			final Point2D m2t = m2 <= ML ? relativePoint(getEnd(), LL * (ML - m2) / length, angle + PI) : null;
+			final Point2D m2o = m2t == null ? null : relativePoint(m2t, we, (angle + endAngle - PI) / 2);
+			
+			if (m1o != null && m2o != null) {
+				final Line l1 = new Line(prev, m1o.getX(), m1o.getY(), m1 - PL);
+				final Line l2 = new Line(l1, m2o.getX(), m2o.getY(), m2 - m1);
+				
+				final Point2D end = offsetEnd(we, endAngle);
+				
+				return new Line(l2, end.getX(), end.getY(), ML - m2);
+			} else if (m1o != null) {
+				final Line l1 = new Line(prev, m1o.getX(), m1o.getY(), m1 - PL);
+				
+				final double w = ws + (ML - m1) * (we - ws) / (m2 - m1);
+				final Point2D end = offsetEnd(w, endAngle);
+				
+				return new Line(l1, end.getX(), end.getY(), ML - m1);
+			} else if (m2o != null) {
+				final Line l2 = new Line(prev, m2o.getX(), m2o.getY(), m2 - PL);
+				
+				final Point2D end = offsetEnd(we, endAngle);
+				
+				return new Line(l2, end.getX(), end.getY(), ML - m2);
+			} else {
+				final double w = ws + (PL - m1 + length) * (we - ws) / (m2 - m1);
+				final Point2D end = offsetEnd(w, endAngle);
+				return new Line(prev, end.getX(), end.getY(), length);
+			}
+		}
+		
+		private Path simpleOffset(Path prev, double w, double endAngle) {
+			final Point2D offset = offsetEnd(w, endAngle);
+			return new Line(prev, offset.getX(), offset.getY(), length);
+		}
+		
+		private Point2D offsetEnd(double w, double endAngle) {
+			final double da2 = minAngleDiff(angle, endAngle) / 2;
+			final double hypotenuse = w / cos(da2);
+			
+			return relativePoint(getEnd(), hypotenuse, angle + PI / 2 + da2);
+		}
+		
+		@Override
+		public SimplePathIterator getIterator() {
+			return new SimplePathIterator(previous.getIteratorInternal(angle), PathIterator.SEG_LINETO, endX, endY, 0, 0, 0,
+			    0);
+		}
+		
+		@Override
+		public Path subpath(double from, double to) {
+			final double PL = previous.getLength();
+			final double ML = PL + length;
+			
+			if (to < PL) {
+				return previous.subpath(from, to);
+			}
+			
+			final Point2D end = to < ML ? getPoint(to) : new Point2D.Double(endX, endY);
+			
+			final double EL = min(ML, to);
+			if (PL <= from) {
+				final Point2D start = getPoint(from);
+				return new Line(new Start(start.getX(), start.getY(), angle), end.getX(), end.getY(), EL - from);
+			} else {
+				return new Line(previous.subpath(from, to), end.getX(), end.getY(), EL - PL);
+			}
+		}
+		
+		@Override
+		public Point2D getPoint(double offset) {
+			final double PL = previous.getLength();
+			final double ML = PL + length;
+			
+			if (offset <= ML && offset >= PL) {
+				final double LL = previous.getEnd().distance(getEnd());
+				return relativePoint(getEnd(), LL * (ML - offset) / length, angle + PI);
+			} else {
+				return previous.getPoint(offset);
+			}
+		}
+		
+		@Override
+		SimplePathIterator getIteratorInternal(double endAngle) {
+			return getIterator();
+		}
+	}
+	
+	// TODO curves are still somewhat broken
+	private static class Curve extends Path {
+		private final Path previous;
+		
+		private final double height;
+		
+		private final double centerX;
+		private final double centerY;
+		private final double centerToFromAngle;
+		
+		private final double endX;
+		private final double endY;
+		
+		private final double fromAngle;
+		private final double fromRadius;
+		private final double toRadius;
+		private final double angle;
+		
+		private final double length;
+		
+		private Curve(Path previous, double r1, double r2, double a, double length, double fromAngle) {
+			this.previous = previous;
+			this.fromAngle = fromAngle;
+			this.fromRadius = r1;
+			this.toRadius = r2;
+			this.angle = a;
+			this.length = length;
+			
+			final Point2D from = previous.getEnd();
+			this.centerToFromAngle = fromAngle - signum(a) * PI / 2;
+			final Point2D center = relativePoint(from, r1, centerToFromAngle + PI);
+			
+			final double toAngle = centerToFromAngle + a;
+			this.endX = center.getX() + r2 * cos(toAngle);
+			this.endY = center.getY() - r2 * sin(toAngle);
+			
+			this.centerX = center.getX();
+			this.centerY = center.getY();
+			
+			final double y = new Line2D.Double(center, from).ptLineDist(endX, endY);
+			this.height = y / sin(angle);
+		}
+		
+		public Curve(Path previous, double r1, double r2, double a, double length) {
+			this(previous, r1, r2, a, length, previous.getEndAngle());
+		}
+		
+		public Point2D getStart() {
+			return previous.getStart();
+		}
+		
+		@Override
+		public Point2D getEnd() {
+			return new Point2D.Double(endX, endY);
+		}
+		
+		@Override
+		public double getEndAngle() {
+			return fromAngle + angle;
+		}
+		
+		@Override
+		public double getLength() {
+			return previous.getLength() + length;
+		}
+		
+		@Override
+		public Path offset(double ws, double m1, double m2, double we) {
+			return offsetInternal(ws, m1, m2, we, previous.getEndAngle() + angle);
+		}
+		
+		@Override
+		Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) {
+			final double PL = previous.getLength();
+			final double ML = PL + length;
+			
+			final Path prev = previous.offsetInternal(ws, m1, m2, we, fromAngle);
+			
+			if (ML <= m1) {
+				return simpleOffset(prev, ws);
+			} else if (m2 <= PL) {
+				return simpleOffset(prev, we);
+			}
+			
+			final double s = signum(angle);
+			
+			if (PL < m1 && m2 < ML) {
+				final double l1 = m1 - PL;
+				final double a1 = angle(l1);
+				final double r1 = radius(a1) - s * ws;
+				
+				final Curve c1 = new Curve(prev, fromRadius - ws, r1, offsetAngle(prev, a1), l1, fromAngle);
+				
+				final double l2 = m2 - m1;
+				final double a2 = angle(l2);
+				final double r2 = radius(a2) - s * we;
+				
+				final Curve c2 = new Curve(c1, r1, r2, a2 - a1, l2);
+				
+				return new Curve(c2, r2, toRadius - s * we, angle - a2, ML - m2);
+			} else if (PL < m1) {
+				final double l1 = m1 - PL;
+				final double a1 = angle(l1);
+				final double r1 = radius(a1) - s * ws;
+				
+				final Curve c1 = new Curve(prev, fromRadius - s * ws, r1, offsetAngle(prev, a1), l1, fromAngle);
+				
+				final double w = ws + (ML - m1) * (we - ws) / (m2 - m1);
+				
+				return new Curve(c1, r1, toRadius - s * w, angle - a1, ML - m1);
+			} else if (m2 < ML) {
+				final double w = ws + (PL - m1) * (we - ws) / (m2 - m1);
+				
+				final double l2 = m2 - PL;
+				final double a2 = angle(l2);
+				final double r2 = radius(a2) - s * we;
+				
+				final Curve c2 = new Curve(prev, fromRadius - s * w, r2, offsetAngle(prev, a2), l2, fromAngle);
+				
+				return new Curve(c2, r2, toRadius - s * we, angle - a2, ML - m2);
+			} else {
+				final double w1 = ws + (PL - m1) * (we - ws) / (m2 - m1);
+				final double w2 = we - (m2 - ML) * (we - ws) / (m2 - m1);
+				
+				return new Curve(prev, fromRadius - s * w1, toRadius - s * w2, offsetAngle(prev, angle), length, fromAngle);
+			}
+		}
+		
+		private double angle(double l) {
+			return l * angle / length;
+		}
+		
+		private double radius(double a) {
+			return hypot(fromRadius * cos(a), height * sin(a));
+		}
+		
+		private double offsetAngle(Path prev, double a) {
+			return a;// + GuiUtil.normalize(previous.getEndAngle()
+			// - prev.getEndAngle());
+		}
+		
+		private Path simpleOffset(Path prev, double w) {
+			final double s = signum(angle);
+			return new Curve(prev, fromRadius - s * w, toRadius - s * w, offsetAngle(prev, angle), length, fromAngle);
+		}
+		
+		@Override
+		public SimplePathIterator getIterator() {
+			return getIteratorInternal(previous.getEndAngle() + angle);
+		}
+		
+		@Override
+		public Path subpath(double from, double to) {
+			final double PL = previous.getLength();
+			final double ML = PL + length;
+			
+			if (to < PL) {
+				return previous.subpath(from, to);
+			}
+			
+			final double toA = to < ML ? angle(to - PL) : angle;
+			final double toR = to < ML ? radius(toA) : toRadius;
+			
+			final double fromA = from > PL ? angle(from - PL) : 0;
+			final double fromR = from > PL ? radius(fromA) : fromRadius;
+			
+			final double a = toA - fromA;
+			final double l = min(ML, to) - max(PL, from);
+			
+			if (from >= PL) {
+				final Point2D start = getPoint(from);
+				final double fa = fromAngle + fromA;
+				
+				return new Curve(new Start(start.getX(), start.getY(), fa), fromR, toR, a, l, fa);
+			} else {
+				return new Curve(previous.subpath(from, to), fromR, toR, a, l, fromAngle);
+			}
+		}
+		
+		@Override
+		public Point2D getPoint(double offset) {
+			final double PL = previous.getLength();
+			final double ML = PL + length;
+			
+			if (offset <= ML && offset >= PL) {
+				final double a = abs(angle(offset - PL));
+				final double w = fromRadius * cos(a);
+				final double h = -height * sin(a);
+				
+				final double r = centerToFromAngle; // rotation angle
+				final double x = w * cos(r) + h * sin(r);
+				final double y = -w * sin(r) + h * cos(r);
+				
+				return new Point2D.Double(centerX + x, centerY + y);
+			} else {
+				return previous.getPoint(offset);
+			}
+		}
+		
+		@Override
+		SimplePathIterator getIteratorInternal(double endAngle) {
+			final Point2D cp1 = relativePoint(previous.getEnd(), cpf(angle, fromRadius), previous.getEndAngle());
+			final Point2D cp2 = relativePoint(getEnd(), cpf(angle, toRadius), endAngle + PI);
+			
+			return new SimplePathIterator(previous.getIteratorInternal(getEndAngle()), PathIterator.SEG_CUBICTO, //
+			    cp1.getX(), cp1.getY(), cp2.getX(), cp2.getY(), endX, endY //
+			);
+			
+		}
+	}
+	
+	private static class Start extends Path {
+		private final double x;
+		private final double y;
+		
+		private final double endAngle;
+		
+		public Start(double x, double y, double endAngle) {
+			this.x = x;
+			this.y = y;
+			this.endAngle = endAngle;
+		}
+		
+		public Start(double x, double y) {
+			this(x, y, Double.NaN);
+		}
+		
+		public Point2D getStart() {
+			return new Point2D.Double(x, y);
+		}
+		
+		public Point2D getEnd() {
+			return new Point2D.Double(x, y);
+		}
+		
+		@Override
+		public double getEndAngle() {
+			if (Double.isNaN(endAngle)) {
+				throw new UnsupportedOperationException();
+			}
+			
+			return endAngle;
+		}
+		
+		@Override
+		public double getLength() {
+			return 0;
+		}
+		
+		@Override
+		public Path offset(double ws, double m1, double m2, double we) {
+			throw new UnsupportedOperationException();
+		}
+		
+		@Override
+		Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) {
+			final Point2D offset = relativePoint(getStart(), ws, endAngle + PI / 2);
+			return new Start(offset.getX(), offset.getY(), endAngle);
+		}
+		
+		@Override
+		public SimplePathIterator getIterator() {
+			return new SimplePathIterator(PathIterator.SEG_MOVETO, x, y, 0, 0, 0, 0);
+		}
+		
+		@Override
+		public Path subpath(double from, double to) {
+			if (from > to) {
+				throw new IllegalArgumentException("from > to");
+			}
+			if (from < 0) {
+				throw new IllegalArgumentException("from < 0");
+			}
+			
+			return this;
+		}
+		
+		@Override
+		public Point2D getPoint(double offset) {
+			if (offset == 0) {
+				return getEnd();
+			} else {
+				throw new IllegalArgumentException(Double.toString(offset));
+			}
+		}
+		
+		@Override
+		SimplePathIterator getIteratorInternal(double endAngle) {
+			return new SimplePathIterator(PathIterator.SEG_MOVETO, x, y, 0, 0, 0, 0);
+		}
+	}
+	
+	public static Path create(double x, double y) {
+		return new Start(x, y);
+	}
+	
+	public Path lineTo(double x, double y, double length) {
+		return new Line(this, x, y, length);
+	}
+	
+	public Path curveTo(double r1, double r2, double a, double length) {
+		return new Curve(this, r1, r2, a, length);
+	}
+	
+	public abstract Path offset(double ws, double m1, double m2, double we);
+	
+	abstract Path offsetInternal(double ws, double m1, double m2, double we, double endAngle);
+	
+	public abstract double getLength();
+	
+	public abstract double getEndAngle();
+	
+	public abstract Point2D getStart();
+	
+	public abstract Point2D getEnd();
+	
+	public abstract SimplePathIterator getIterator();
+	
+	abstract SimplePathIterator getIteratorInternal(double endAngle);
+	
+	public abstract Path subpath(double from, double to);
+	
+	public abstract Point2D getPoint(double offset);
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/ReversePathIterator.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/ReversePathIterator.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/ReversePathIterator.java	(revision 25606)
@@ -0,0 +1,150 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import java.awt.geom.PathIterator;
+import java.util.Arrays;
+
+/**
+ * Path iterator which reverses the path of a given iterator.
+ * 
+ * <p>
+ * The given (unreversed) iterator must start with a {@code PathIterator.SEG_MOVETO} and must not
+ * contain any {@code PathIterator.SEG_CLOSE}. This class is intended for use with iterators
+ * returned by {@code Path.getIterator} which has exactly those properties.
+ * </p>
+ * 
+ * @author Ben Schulz
+ */
+class ReversePathIterator implements PathIterator {
+	private static final int[] COUNTS = {
+	    2, // SEG_MOVETO = 0
+	    2, // SEG_LINETO = 1
+	    4, // SEG_QUADTO = 2
+	    6, // SEG_CUBICTO = 3
+	    0, // SEG_CLOSE = 4
+	};
+	
+	public static ReversePathIterator reverse(PathIterator it) {
+		return new ReversePathIterator(it);
+	}
+	
+	private static int[] reverseTypes(int[] types, int length) {
+		if (length > 0 && types[0] != SEG_MOVETO) {
+			// the last segment of the reversed path is not defined
+			throw new IllegalArgumentException("Can not reverse path without initial SEG_MOVETO.");
+		}
+		
+		final int[] result = new int[length];
+		
+		result[0] = SEG_MOVETO;
+		
+		int lower = 1;
+		int upper = length - 1;
+		
+		while (lower <= upper) {
+			result[lower] = types[upper];
+			result[upper] = types[lower];
+			
+			++lower;
+			--upper;
+		}
+		
+		return result;
+	}
+	
+	private static double[] reverseCoords(double[] coords, int length) {
+		final double[] result = new double[length];
+		
+		int lower = 0;
+		int upper = length - 2;
+		
+		while (lower <= upper) {
+			result[lower] = coords[upper];
+			result[lower + 1] = coords[upper + 1];
+			result[upper] = coords[lower];
+			result[upper + 1] = coords[lower + 1];
+			
+			lower += 2;
+			upper -= 2;
+		}
+		
+		return result;
+	}
+	
+	private final int winding;
+	
+	private final int[] types;
+	private int typesIndex = 0;
+	
+	private final double[] coords;
+	private int coordsIndex = 0;
+	
+	private ReversePathIterator(PathIterator it) {
+		this.winding = it.getWindingRule();
+		
+		double[] tmpCoords = new double[62];
+		int[] tmpTypes = new int[11];
+		
+		int tmpCoordsI = 0;
+		int tmpTypesI = 0;
+		
+		while (!it.isDone()) {
+			if (tmpTypesI >= tmpTypes.length) {
+				tmpTypes = Arrays.copyOf(tmpTypes, 2 * tmpTypes.length);
+			}
+			
+			final double[] cs = new double[6];
+			final int t = it.currentSegment(cs);
+			tmpTypes[tmpTypesI++] = t;
+			final int count = COUNTS[t];
+			
+			if (tmpCoordsI + count > tmpCoords.length) {
+				tmpCoords = Arrays.copyOf(tmpCoords, 2 * tmpCoords.length);
+			}
+			System.arraycopy(cs, 0, tmpCoords, tmpCoordsI, count);
+			tmpCoordsI += count;
+			
+			it.next();
+		}
+		
+		this.types = reverseTypes(tmpTypes, tmpTypesI);
+		this.coords = reverseCoords(tmpCoords, tmpCoordsI);
+	}
+	
+	@Override
+	public int getWindingRule() {
+		return winding;
+	}
+	
+	@Override
+	public boolean isDone() {
+		return typesIndex >= types.length;
+	}
+	
+	@Override
+	public void next() {
+		coordsIndex += COUNTS[types[typesIndex]];
+		++typesIndex;
+	}
+	
+	@Override
+	public int currentSegment(float[] coords) {
+		final double[] tmp = new double[6];
+		final int type = currentSegment(tmp);
+		
+		coords[0] = (float) tmp[0];
+		coords[1] = (float) tmp[1];
+		coords[2] = (float) tmp[2];
+		coords[3] = (float) tmp[3];
+		coords[4] = (float) tmp[4];
+		coords[5] = (float) tmp[5];
+		
+		return type;
+	}
+	
+	@Override
+	public int currentSegment(double[] coords) {
+		final int type = types[typesIndex];
+		System.arraycopy(this.coords, coordsIndex, coords, 0, COUNTS[type]);
+		return type;
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/RoadGui.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/RoadGui.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/RoadGui.java	(revision 25606)
@@ -0,0 +1,814 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import static java.lang.Math.PI;
+import static java.lang.Math.abs;
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import static java.lang.Math.tan;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.angle;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.area;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.closest;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.intersection;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.line;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.loc;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.minAngleDiff;
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.relativePoint;
+
+import java.awt.AlphaComposite;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.Stroke;
+import java.awt.geom.Area;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.plugins.turnlanes.CollectionUtils;
+import org.openstreetmap.josm.plugins.turnlanes.model.Lane;
+import org.openstreetmap.josm.plugins.turnlanes.model.Road;
+import org.openstreetmap.josm.plugins.turnlanes.model.Utils;
+
+class RoadGui {
+	private final class Extender extends InteractiveElement {
+		private final Way way;
+		
+		private final Line2D line;
+		
+		public Extender(Way way, double angle) {
+			this.way = way;
+			this.line = new Line2D.Double(a.getPoint(), relativePoint(a.getPoint(), getContainer().getLaneWidth() * 4, angle));
+		}
+		
+		@Override
+		void paint(Graphics2D g2d, State state) {
+			g2d.setStroke(getContainer().getConnectionStroke());
+			g2d.setColor(Color.CYAN);
+			g2d.draw(line);
+		}
+		
+		@Override
+		boolean contains(Point2D p, State state) {
+			final BasicStroke stroke = (BasicStroke) getContainer().getConnectionStroke();
+			final double strokeWidth = stroke.getLineWidth();
+			
+			final Point2D closest = closest(line, p);
+			return p.distance(closest) <= strokeWidth / 2;
+		}
+		
+		@Override
+		State click(State old) {
+			getModel().extend(way);
+			return new State.Invalid(old);
+		}
+		
+		@Override
+		Type getType() {
+			return Type.EXTENDER;
+		}
+		
+		@Override
+		int getZIndex() {
+			return 0;
+		}
+		
+	}
+	
+	private final class LaneAdder extends InteractiveElement {
+		private final Road.End end;
+		private final boolean left;
+		
+		private final Point2D center;
+		private final Ellipse2D background;
+		
+		public LaneAdder(Road.End end, boolean left) {
+			this.end = end;
+			this.left = left;
+			
+			final double a = getAngle(end) + PI;
+			final Point2D lc = getLeftCorner(end);
+			final Point2D rc = getRightCorner(end);
+			
+			final double r = connectorRadius;
+			final double cx;
+			final double cy;
+			if (left) {
+				final JunctionGui j = getContainer().getGui(end.getJunction());
+				final Point2D i = intersection(line(j.getPoint(), a), new Line2D.Double(lc, rc));
+				
+				cx = i.getX() + 21d / 16 * r * (2 * cos(a) + cos(a - PI / 2));
+				cy = i.getY() - 21d / 16 * r * (2 * sin(a) + sin(a - PI / 2));
+			} else {
+				cx = rc.getX() + 21d / 16 * r * (2 * cos(a) + cos(a + PI / 2));
+				cy = rc.getY() - 21d / 16 * r * (2 * sin(a) + sin(a + PI / 2));
+			}
+			
+			center = new Point2D.Double(cx, cy);
+			background = new Ellipse2D.Double(cx - r, cy - r, 2 * r, 2 * r);
+		}
+		
+		@Override
+		void paint(Graphics2D g2d, State state) {
+			if (!isVisible(state)) {
+				return;
+			}
+			
+			g2d.setColor(Color.DARK_GRAY);
+			g2d.fill(background);
+			
+			final double l = 2 * connectorRadius / 3;
+			final Line2D v = new Line2D.Double(center.getX(), center.getY() - l, center.getX(), center.getY() + l);
+			final Line2D h = new Line2D.Double(center.getX() - l, center.getY(), center.getX() + l, center.getY());
+			
+			g2d.setStroke(new BasicStroke((float) (connectorRadius / 5)));
+			g2d.setColor(Color.WHITE);
+			g2d.draw(v);
+			g2d.draw(h);
+		}
+		
+		private boolean isVisible(State state) {
+			return end.getJunction().equals(state.getJunction().getModel());
+		}
+		
+		@Override
+		boolean contains(Point2D p, State state) {
+			return isVisible(state) && background.contains(p);
+		}
+		
+		@Override
+		Type getType() {
+			return Type.LANE_ADDER;
+		}
+		
+		@Override
+		int getZIndex() {
+			return 2;
+		}
+		
+		@Override
+		public State click(State old) {
+			end.addExtraLane(left);
+			return new State.Invalid(old);
+		}
+	}
+	
+	final class IncomingConnector extends InteractiveElement {
+		private final boolean forward;
+		private final Point2D center = new Point2D.Double();
+		private final Ellipse2D circle = new Ellipse2D.Double();
+		
+		private final List<LaneGui> lanes = new ArrayList<LaneGui>();
+		
+		private IncomingConnector(boolean forward) {
+			this.forward = forward;
+		}
+		
+		@Override
+		public void paintBackground(Graphics2D g2d, State state) {
+			if (isActive(state)) {
+				final Composite old = g2d.getComposite();
+				g2d.setComposite(((AlphaComposite) old).derive(0.2f));
+				
+				g2d.setColor(new Color(255, 127, 31));
+				
+				for (LaneGui l : lanes) {
+					l.fill(g2d);
+				}
+				
+				g2d.setComposite(old);
+			}
+		}
+		
+		@Override
+		public void paint(Graphics2D g2d, State state) {
+			if (isVisible(state)) {
+				final Composite old = g2d.getComposite();
+				if (isActive(state)) {
+					g2d.setComposite(((AlphaComposite) old).derive(1f));
+				}
+				
+				g2d.setColor(Color.LIGHT_GRAY);
+				g2d.fill(circle);
+				
+				g2d.setComposite(old);
+			}
+		}
+		
+		private boolean isActive(State state) {
+			if (!(state instanceof State.IncomingActive)) {
+				return false;
+			}
+			
+			final Road.End roadEnd = ((State.IncomingActive) state).getRoadEnd();
+			
+			return roadEnd.equals(getRoadEnd());
+		}
+		
+		private boolean isVisible(State state) {
+			if (!getRoadEnd().getJunction().equals(state.getJunction().getModel())) {
+				return false;
+			}
+			
+			// must be at least one lane in that direction
+			final boolean reverse = getRoadEnd().isToEnd();
+			for (Lane l : getModel().getLanes()) {
+				if (l.isReverse() == reverse) {
+					return true;
+				}
+			}
+			
+			return false;
+		}
+		
+		@Override
+		public boolean contains(Point2D p, State state) {
+			if (!isVisible(state)) {
+				return false;
+			} else if (circle.contains(p)) {
+				return true;
+			}
+			
+			for (LaneGui l : lanes) {
+				if (l.contains(p)) {
+					return true;
+				}
+			}
+			
+			return false;
+		}
+		
+		@Override
+		public Type getType() {
+			return Type.INCOMING_CONNECTOR;
+		}
+		
+		@Override
+		public State activate(State old) {
+			return new State.IncomingActive(old.getJunction(), getRoadEnd());
+		}
+		
+		public Point2D getCenter() {
+			return (Point2D) center.clone();
+		}
+		
+		void move(double x, double y) {
+			final double r = connectorRadius;
+			
+			center.setLocation(x, y);
+			circle.setFrame(x - r, y - r, 2 * r, 2 * r);
+		}
+		
+		Road.End getRoadEnd() {
+			return forward ? getModel().getFromEnd() : getModel().getToEnd();
+		}
+		
+		@Override
+		int getZIndex() {
+			return 1;
+		}
+		
+		public void add(LaneGui lane) {
+			lanes.add(lane);
+		}
+	}
+	
+	// TODO rework to be a SegmentGui (with getModel())
+	private final class Segment {
+		final Point2D to;
+		final Point2D from;
+		
+		final Segment prev;
+		final Segment next;
+		
+		final double length;
+		final double angle;
+		
+		public Segment(Segment next, List<Point2D> bends, JunctionGui a) {
+			final Point2D head = (Point2D) bends.get(0).clone();
+			final List<Point2D> tail = bends.subList(1, bends.size());
+			
+			this.next = next;
+			this.to = head;
+			this.from = (Point2D) (tail.isEmpty() ? a.getPoint() : tail.get(0)).clone();
+			this.prev = tail.isEmpty() ? null : new Segment(this, tail, a);
+			this.length = from.distance(to);
+			this.angle = angle(from, to);
+			
+			// TODO create a factory method for the segments list and pass it to
+			// the constructor(s)
+			segments.add(this);
+		}
+		
+		public Segment(JunctionGui b, List<Point2D> bends, JunctionGui a) {
+			this((Segment) null, prepended(bends, (Point2D) b.getPoint().clone()), a);
+		}
+		
+		private double getFromOffset() {
+			return prev == null ? 0 : prev.getFromOffset() + prev.length;
+		}
+		
+		public double getOffset(double x, double y) {
+			return getOffsetInternal(new Point2D.Double(x, y), -1, Double.POSITIVE_INFINITY);
+		}
+		
+		private double getOffsetInternal(Point2D p, double offset, double quality) {
+			final Point2D closest = closest(new Line2D.Double(from, to), p);
+			final double myQuality = closest.distance(p);
+			
+			if (myQuality < quality) {
+				quality = myQuality;
+				
+				final Line2D normal = line(p, angle + PI / 2);
+				final Point2D isect = intersection(normal, new Line2D.Double(from, to));
+				final double d = from.distance(isect);
+				final boolean negative = Math.abs(angle(from, isect) - angle) > 1;
+				
+				offset = getFromOffset() + (negative ? -1 : 1) * d;
+			}
+			
+			return next == null ? offset : next.getOffsetInternal(p, offset, quality);
+		}
+		
+		public Path append(Path path, boolean forward, double offset) {
+			if (ROUND_CORNERS) {
+				final Segment n = forward ? prev : next;
+				final Point2D s = forward ? to : from;
+				final Point2D e = forward ? from : to;
+				
+				if (n == null) {
+					return path.lineTo(e.getX(), e.getY(), length - offset);
+				}
+				
+				final double a = minAngleDiff(angle, n.angle);
+				final double d = 3 * outerMargin + getWidth(getModel().getToEnd(), (forward && a < 0) || (!forward && a > 0));
+				final double l = d * tan(abs(a));
+				
+				if (length - offset < l / 2 || n.length < l / 2) {
+					return n.append(path.lineTo(e.getX(), e.getY(), length - offset), forward, 0);
+				} else {
+					final Point2D p = relativePoint(e, l / 2, angle(e, s));
+					
+					final Path line = path.lineTo(p.getX(), p.getY(), length - l / 2 - offset);
+					final Path curve = line.curveTo(d, d, a, l);
+					
+					return n.append(curve, forward, l / 2);
+				}
+			} else if (forward) {
+				final Path tmp = path.lineTo(from.getX(), from.getY(), length);
+				return prev == null ? tmp : prev.append(tmp, forward, 0);
+			} else {
+				final Path tmp = path.lineTo(to.getX(), to.getY(), length);
+				return next == null ? tmp : next.append(tmp, forward, 0);
+			}
+		}
+	}
+	
+	/**
+	 * This should become a setting, but rounding is (as of yet) still slightly buggy and a low
+	 * priority.
+	 */
+	private static final boolean ROUND_CORNERS = false;
+	
+	private static final List<Point2D> prepended(List<Point2D> bends, Point2D point) {
+		final List<Point2D> result = new ArrayList<Point2D>(bends.size() + 1);
+		result.add(point);
+		result.addAll(bends);
+		return result;
+	}
+	
+	private final GuiContainer container;
+	private final double innerMargin;
+	private final double outerMargin;
+	
+	private final float lineWidth;
+	private final Stroke regularStroke;
+	private final Stroke dashedStroke;
+	
+	private final JunctionGui a;
+	private final JunctionGui b;
+	private final double length;
+	
+	private final IncomingConnector incomingA = new IncomingConnector(true);
+	private final IncomingConnector incomingB = new IncomingConnector(false);
+	
+	private final Road road;
+	private final List<LaneGui> lanes = new ArrayList<LaneGui>();
+	private final List<Segment> segments = new ArrayList<Segment>();
+	
+	final double connectorRadius;
+	
+	public RoadGui(GuiContainer container, Road r) {
+		this.container = container;
+		
+		this.a = container.getGui(r.getFromEnd().getJunction());
+		this.b = container.getGui(r.getToEnd().getJunction());
+		
+		this.road = r;
+		
+		final List<Point2D> bends = new ArrayList<Point2D>();
+		final List<Node> nodes = r.getRoute().getNodes();
+		for (int i = nodes.size() - 2; i > 0; --i) {
+			bends.add(container.translateAndScale(loc(nodes.get(i))));
+		}
+		
+		// they add themselves to this.segments
+		new Segment(b, bends, a);
+		
+		double l = 0;
+		for (Segment s : segments) {
+			l += s.length;
+		}
+		
+		this.length = l;
+		
+		for (Lane lane : r.getLanes()) {
+			final LaneGui gui = new LaneGui(this, lane);
+			lanes.add(gui);
+			(lane.isReverse() ? incomingB : incomingA).add(gui);
+		}
+		
+		this.innerMargin = getLaneCount(true) > 0 && getLaneCount(false) > 0 ? 1 * container.getLaneWidth() / 15 : 0;
+		this.outerMargin = container.getLaneWidth() / 6;
+		this.connectorRadius = 3 * container.getLaneWidth() / 8;
+		this.lineWidth = (float) (container.getLaneWidth() / 30);
+		this.regularStroke = new BasicStroke(2 * lineWidth);
+		this.dashedStroke = new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 10f, new float[] {
+		    (float) (container.getLaneWidth() / 2), (float) (container.getLaneWidth() / 3)
+		}, 0);
+	}
+	
+	public JunctionGui getA() {
+		return a;
+	}
+	
+	public JunctionGui getB() {
+		return b;
+	}
+	
+	private int laneMiddle() {
+		int i = 0;
+		while (lanes.size() > i && lanes.get(i).getModel().isReverse()) {
+			++i;
+		}
+		return i;
+	}
+	
+	public Line2D getLeftCurb(Road.End end) {
+		return GuiUtil.line(getCorner(end, true), getAngle(end) + PI);
+	}
+	
+	public Line2D getRightCurb(Road.End end) {
+		return GuiUtil.line(getCorner(end, false), getAngle(end) + PI);
+	}
+	
+	private Point2D getLeftCorner(Road.End end) {
+		return getCorner(end, true);
+	}
+	
+	private Point2D getRightCorner(Road.End end) {
+		return getCorner(end, false);
+	}
+	
+	private Point2D getCorner(Road.End end, boolean left) {
+		final JunctionGui j = end.isFromEnd() ? a : b;
+		final double w = left ? getWidth(end, true) : getWidth(end, false);
+		final double s = (left ? 1 : -1);
+		final double a = getAngle(end) + PI;
+		final double t = left ? j.getLeftTrim(end) : j.getRightTrim(end);
+		
+		final double dx = s * cos(PI / 2 - a) * w + cos(a) * t;
+		final double dy = s * sin(PI / 2 - a) * w - sin(a) * t;
+		
+		return new Point2D.Double(j.x + dx, j.y + dy);
+	}
+	
+	private double getWidth(Road.End end, boolean left) {
+		if (!end.getRoad().equals(road)) {
+			throw new IllegalArgumentException();
+		}
+		
+		final int lcForward = getLaneCount(false);
+		final int lcBackward = getLaneCount(true);
+		
+		final double LW = getContainer().getLaneWidth();
+		final double M = innerMargin + outerMargin;
+		
+		if (end.isToEnd()) {
+			return (left ? lcBackward : lcForward) * LW + M;
+		} else {
+			return (left ? lcForward : lcBackward) * LW + M;
+		}
+	}
+	
+	private int getLaneCount(boolean reverse) {
+		int result = 0;
+		
+		for (LaneGui l : lanes) {
+			if (l.getModel().isReverse() == reverse) {
+				++result;
+			}
+		}
+		
+		return result;
+	}
+	
+	List<InteractiveElement> paint(Graphics2D g2d) {
+		final List<InteractiveElement> result = new ArrayList<InteractiveElement>();
+		
+		result.addAll(paintLanes(g2d));
+		result.addAll(laneAdders());
+		result.addAll(extenders());
+		
+		g2d.setColor(Color.RED);
+		for (Segment s : segments) {
+			g2d.fill(new Ellipse2D.Double(s.from.getX() - 1, s.from.getY() - 1, 2, 2));
+		}
+		
+		return result;
+	}
+	
+	private List<LaneAdder> laneAdders() {
+		final List<LaneAdder> result = new ArrayList<LaneAdder>(4);
+		
+		boolean to = false;
+		boolean from = false;
+		for (Lane l : getModel().getLanes()) {
+			to = to || (!l.isReverse() && !l.isExtra());
+			from = from || (l.isReverse() && !l.isExtra());
+		}
+		
+		if (to) {
+			result.add(new LaneAdder(getModel().getToEnd(), true));
+			result.add(new LaneAdder(getModel().getToEnd(), false));
+		}
+		
+		if (from) {
+			result.add(new LaneAdder(getModel().getFromEnd(), true));
+			result.add(new LaneAdder(getModel().getFromEnd(), false));
+		}
+		
+		return result;
+	}
+	
+	private List<Extender> extenders() {
+		if (!getModel().isExtendable()) {
+			return Collections.emptyList();
+		}
+		
+		final List<Extender> result = new ArrayList<Extender>();
+		
+		final Node n = a.getModel().getNode();
+		for (OsmPrimitive p : n.getReferrers()) {
+			if (p.getType() != OsmPrimitiveType.WAY) {
+				continue;
+			}
+			
+			Way w = (Way) p;
+			if (w.isFirstLastNode(n) && w.getNodesCount() > 1 && Utils.isRoad(w)) {
+				if (getModel().getRoute().getFirstSegment().getWay().equals(w)) {
+					continue;
+				}
+				
+				final Node nextNode = w.firstNode().equals(n) ? w.getNode(1) : w.getNode(w.getNodesCount() - 2);
+				final Point2D nextNodeLoc = getContainer().translateAndScale(loc(nextNode));
+				result.add(new Extender(w, angle(a.getPoint(), nextNodeLoc)));
+			}
+		}
+		
+		return result;
+	}
+	
+	public Road getModel() {
+		return road;
+	}
+	
+	public IncomingConnector getConnector(Road.End end) {
+		return end.isFromEnd() ? incomingA : incomingB;
+	}
+	
+	private List<InteractiveElement> paintLanes(Graphics2D g2d) {
+		final Path2D middleLines = new Path2D.Double();
+		
+		g2d.setStroke(regularStroke);
+		
+		final boolean forward = getLaneCount(false) > 0;
+		final boolean backward = getLaneCount(true) > 0;
+		
+		final Path2D middleArea;
+		if (forward && backward) {
+			paintLanes(g2d, middleLines, true);
+			paintLanes(g2d, middleLines, false);
+			
+			middleLines.closePath();
+			middleArea = middleLines;
+			g2d.setColor(new Color(160, 160, 160));
+		} else if (forward || backward) {
+			paintLanes(g2d, middleLines, forward);
+			
+			middleArea = new Path2D.Double();
+			middleArea.append(middleLines.getPathIterator(null), false);
+			middleArea.append(middlePath(backward).offset(outerMargin, -1, -1, outerMargin).getIterator(), true);
+			middleArea.closePath();
+			g2d.setColor(Color.GRAY);
+		} else {
+			throw new AssertionError();
+		}
+		
+		g2d.fill(middleArea);
+		g2d.setColor(Color.WHITE);
+		g2d.draw(middleLines);
+		
+		moveIncoming(getModel().getFromEnd());
+		moveIncoming(getModel().getToEnd());
+		
+		final int laneMiddle = laneMiddle();
+		for (int i = laneMiddle; i < lanes.size(); ++i) {
+			moveOutgoing(lanes.get(i), i - laneMiddle);
+		}
+		for (int i = laneMiddle - 1; i >= 0; --i) {
+			moveOutgoing(lanes.get(i), laneMiddle - i - 1);
+		}
+		
+		final List<InteractiveElement> result = new ArrayList<InteractiveElement>();
+		
+		result.add(incomingA);
+		result.add(incomingB);
+		for (LaneGui l : lanes) {
+			result.add(l.outgoing);
+			
+			if (l.getModel().isExtra()) {
+				result.add(l.lengthSlider);
+			}
+		}
+		
+		return result;
+	}
+	
+	private void paintLanes(Graphics2D g2d, Path2D middleLines, boolean forward) {
+		final Shape clip = clip();
+		g2d.clip(clip);
+		
+		final Path middle = middlePath(forward);
+		
+		Path innerPath = middle.offset(innerMargin, -1, -1, innerMargin);
+		final List<Path> linePaths = new ArrayList<Path>();
+		linePaths.add(innerPath);
+		
+		for (LaneGui l : lanes(forward)) {
+			l.setClip(clip);
+			innerPath = l.recalculate(innerPath, middleLines);
+			linePaths.add(innerPath);
+		}
+		
+		final Path2D area = new Path2D.Double();
+		area(area, middle, innerPath.offset(outerMargin, -1, -1, outerMargin));
+		g2d.setColor(Color.GRAY);
+		g2d.fill(area);
+		
+		g2d.setColor(Color.WHITE);
+		final Path2D lines = new Path2D.Double();
+		lines.append(innerPath.getIterator(), false);
+		g2d.draw(lines);
+		
+		// g2d.setColor(new Color(32, 128, 192));
+		g2d.setColor(Color.WHITE);
+		g2d.setStroke(dashedStroke);
+		for (Path p : linePaths) {
+			lines.reset();
+			lines.append(p.getIterator(), false);
+			g2d.draw(lines);
+		}
+		g2d.setStroke(regularStroke);
+		
+		// g2d.setColor(new Color(32, 128, 192));
+		// lines.reset();
+		// lines.append(middle.getIterator(), false);
+		// g2d.draw(lines);
+		
+		g2d.setClip(null);
+	}
+	
+	private Shape clip() {
+		final Area clip = new Area(new Rectangle2D.Double(-100000, -100000, 200000, 200000));
+		clip.subtract(new Area(negativeClip(true)));
+		clip.subtract(new Area(negativeClip(false)));
+		
+		return clip;
+	}
+	
+	private Shape negativeClip(boolean forward) {
+		final Road.End end = forward ? getModel().getToEnd() : getModel().getFromEnd();
+		final JunctionGui j = forward ? b : a;
+		
+		final Line2D lc = getLeftCurb(end);
+		final Line2D rc = getRightCurb(end);
+		
+		final Path2D negativeClip = new Path2D.Double();
+		
+		final double d = rc.getP1().distance(j.getPoint()) + lc.getP1().distance(j.getPoint());
+		
+		final Point2D r1 = relativePoint(rc.getP1(), 1, angle(lc.getP1(), rc.getP1()));
+		final Point2D r2 = relativePoint(r1, d, angle(rc) + PI);
+		final Point2D l1 = relativePoint(lc.getP1(), 1, angle(rc.getP1(), lc.getP1()));
+		final Point2D l2 = relativePoint(l1, d, angle(lc) + PI);
+		
+		negativeClip.moveTo(r1.getX(), r1.getY());
+		negativeClip.lineTo(r2.getX(), r2.getY());
+		negativeClip.lineTo(l2.getX(), l2.getY());
+		negativeClip.lineTo(l1.getX(), l1.getY());
+		negativeClip.closePath();
+		
+		return negativeClip;
+	}
+	
+	private Iterable<LaneGui> lanes(boolean forward) {
+		final int laneMiddle = laneMiddle();
+		
+		return forward ? lanes.subList(laneMiddle, lanes.size()) : CollectionUtils.reverse(lanes.subList(0, laneMiddle));
+	}
+	
+	private Path middlePath(boolean forward) {
+		final Path path = forward ? Path.create(b.x, b.y) : Path.create(a.x, a.y);
+		final Segment first = forward ? segments.get(segments.size() - 1) : segments.get(0);
+		
+		return first.append(path, forward, 0);
+	}
+	
+	private void moveIncoming(Road.End end) {
+		final Point2D lc = getLeftCorner(end);
+		final Point2D rc = getRightCorner(end);
+		final Line2D cornerLine = new Line2D.Double(lc, rc);
+		
+		final double a = getAngle(end);
+		final Line2D roadLine = line(getContainer().getGui(end.getJunction()).getPoint(), a);
+		
+		final Point2D i = intersection(roadLine, cornerLine);
+		// TODO fix depending on angle(i, lc)
+		final double offset = innerMargin + (getWidth(end, true) - innerMargin - outerMargin) / 2;
+		final Point2D loc = relativePoint(i, offset, angle(i, lc));
+		
+		getConnector(end).move(loc.getX(), loc.getY());
+	}
+	
+	private void moveOutgoing(LaneGui lane, int offset) {
+		final Road.End end = lane.getModel().getOutgoingRoadEnd();
+		
+		final Point2D lc = getLeftCorner(end);
+		final Point2D rc = getRightCorner(end);
+		final Line2D cornerLine = new Line2D.Double(lc, rc);
+		
+		final double a = getAngle(end);
+		final Line2D roadLine = line(getContainer().getGui(end.getJunction()).getPoint(), a);
+		
+		final Point2D i = intersection(roadLine, cornerLine);
+		// TODO fix depending on angle(i, rc)
+		final double d = innerMargin + (2 * offset + 1) * getContainer().getLaneWidth() / 2;
+		final Point2D loc = relativePoint(i, d, angle(i, rc));
+		
+		lane.outgoing.move(loc.getX(), loc.getY());
+	}
+	
+	public double getAngle(Road.End end) {
+		if (!getModel().equals(end.getRoad())) {
+			throw new IllegalArgumentException();
+		}
+		
+		if (end.isToEnd()) {
+			return segments.get(segments.size() - 1).angle;
+		} else {
+			final double angle = segments.get(0).angle;
+			return angle > PI ? angle - PI : angle + PI;
+		}
+	}
+	
+	public double getWidth(Road.End end) {
+		return getWidth(end, true) + getWidth(end, false);
+	}
+	
+	public double getLength() {
+		return length;
+	}
+	
+	public List<LaneGui> getLanes() {
+		return Collections.unmodifiableList(lanes);
+	}
+	
+	public double getOffset(double x, double y) {
+		return segments.get(0).getOffset(x, y);
+	}
+	
+	public GuiContainer getContainer() {
+		return container;
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/State.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/State.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/State.java	(revision 25606)
@@ -0,0 +1,89 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import org.openstreetmap.josm.plugins.turnlanes.model.Road;
+
+interface State {
+	public class Invalid implements State {
+		private final State wrapped;
+		
+		public Invalid(State wrapped) {
+			this.wrapped = wrapped;
+		}
+		
+		public JunctionGui getJunction() {
+			return wrapped.getJunction();
+		}
+		
+		public State unwrap() {
+			return wrapped;
+		}
+	}
+	
+	public class Dirty implements State {
+		private final State wrapped;
+		
+		public Dirty(State wrapped) {
+			this.wrapped = wrapped;
+		}
+		
+		public JunctionGui getJunction() {
+			return wrapped.getJunction();
+		}
+		
+		public State unwrap() {
+			return wrapped;
+		}
+	}
+	
+	class Default implements State {
+		private final JunctionGui junction;
+		
+		public Default(JunctionGui junction) {
+			this.junction = junction;
+		}
+		
+		public JunctionGui getJunction() {
+			return junction;
+		}
+	}
+	
+	class IncomingActive implements State {
+		private final JunctionGui junction;
+		private final Road.End roadEnd;
+		
+		public IncomingActive(JunctionGui junction, Road.End roadEnd) {
+			this.junction = junction;
+			this.roadEnd = roadEnd;
+		}
+		
+		public Road.End getRoadEnd() {
+			return roadEnd;
+		}
+		
+		@Override
+		public JunctionGui getJunction() {
+			return junction;
+		}
+	}
+	
+	class OutgoingActive implements State {
+		private final JunctionGui junction;
+		private final LaneGui lane;
+		
+		public OutgoingActive(JunctionGui junction, LaneGui lane) {
+			this.junction = junction;
+			this.lane = lane;
+		}
+		
+		public LaneGui getLane() {
+			return lane;
+		}
+		
+		@Override
+		public JunctionGui getJunction() {
+			return junction;
+		}
+	}
+	
+	JunctionGui getJunction();
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/TurnLanesDialog.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/TurnLanesDialog.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/TurnLanesDialog.java	(revision 25606)
@@ -0,0 +1,142 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.loc;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.CardLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.util.Collection;
+
+import javax.swing.Action;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.SelectionChangedListener;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.gui.SideButton;
+import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
+import org.openstreetmap.josm.plugins.turnlanes.model.Junction;
+import org.openstreetmap.josm.plugins.turnlanes.model.ModelContainer;
+
+public class TurnLanesDialog extends ToggleDialog {
+	private final Action editAction = new JosmAction(tr("Edit"), "dialogs/edit",
+	    tr("Edit turn relations and lane lengths for selected node."), null, true) {
+		
+		private static final long serialVersionUID = 4114119073563457706L;
+		
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			final CardLayout cl = (CardLayout) body.getLayout();
+			cl.show(body, CARD_EDIT);
+			editing = true;
+		}
+	};
+	private final Action validateAction = new JosmAction(tr("Validate"), "dialogs/validator",
+	    tr("Validate turn- and lane-length-relations for consistency."), null, true) {
+		
+		private static final long serialVersionUID = 7510740945725851427L;
+		
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			final CardLayout cl = (CardLayout) body.getLayout();
+			cl.show(body, CARD_VALIDATE);
+			editing = false;
+		}
+	};
+	
+	private static final long serialVersionUID = -1998375221636611358L;
+	
+	private static final String CARD_EDIT = "EDIT";
+	private static final String CARD_VALIDATE = "VALIDATE";
+	private static final String CARD_ERROR = "ERROR";
+	
+	private final JPanel body = new JPanel();
+	private final JunctionPane junctionPane = new JunctionPane(null);
+	private final JLabel error = new JLabel();
+	
+	private boolean editing = true;
+	
+	public TurnLanesDialog() {
+		super(tr("Turn Lanes"), "turnlanes.png", tr("Edit turn lanes"), null, 200);
+		
+		DataSet.addSelectionListener(new SelectionChangedListener() {
+			@Override
+			public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
+				if (newSelection.size() != 1) {
+					return;
+				}
+				
+				final OsmPrimitive p = newSelection.iterator().next();
+				
+				if (p.getType() == OsmPrimitiveType.NODE) {
+					final Node n = (Node) p;
+					
+					final ModelContainer mc;
+					final Junction j;
+					
+					try {
+						mc = ModelContainer.create(n);
+						j = mc.getJunction(n);
+					} catch (RuntimeException e) {
+						displayError(e);
+						
+						return;
+					}
+					
+					final EastNorth en = Main.proj.latlon2eastNorth(n.getCoor());
+					final EastNorth rel = new EastNorth(en.getX() + 1, en.getY());
+					
+					// meters per source unit
+					final double mpsu = Main.proj.eastNorth2latlon(rel).greatCircleDistance(n.getCoor());
+					final GuiContainer model = new GuiContainer(loc(n), mpsu);
+					
+					setJunction(model.getGui(j));
+				}
+			}
+			
+		});
+		
+		final JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 4, 4));
+		buttonPanel.add(new SideButton(editAction));
+		buttonPanel.add(new SideButton(validateAction));
+		
+		body.setLayout(new CardLayout(4, 4));
+		
+		add(buttonPanel, BorderLayout.SOUTH);
+		add(body, BorderLayout.CENTER);
+		
+		body.add(junctionPane, CARD_EDIT);
+		body.add(new ValidationPanel(), CARD_VALIDATE);
+		body.add(error, CARD_ERROR);
+		editAction.actionPerformed(null);
+	}
+	
+	void displayError(RuntimeException e) {
+		if (editing) {
+			// e.printStackTrace();
+			
+			error.setText("<html>An error occured while constructing the model."
+			    + " Please run the validator to make sure the data is consistent.<br><br>Error: " + e.getMessage()
+			    + "</html>");
+			
+			final CardLayout cl = (CardLayout) body.getLayout();
+			cl.show(body, CARD_ERROR);
+		}
+	}
+	
+	void setJunction(JunctionGui j) {
+		if (j != null && editing) {
+			junctionPane.setJunction(j);
+			final CardLayout cl = (CardLayout) body.getLayout();
+			cl.show(body, CARD_EDIT);
+		}
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/ValidationPanel.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/ValidationPanel.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/gui/ValidationPanel.java	(revision 25606)
@@ -0,0 +1,139 @@
+package org.openstreetmap.josm.plugins.turnlanes.gui;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.Action;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.DefaultTableModel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.SideButton;
+import org.openstreetmap.josm.plugins.turnlanes.model.Issue;
+import org.openstreetmap.josm.plugins.turnlanes.model.Validator;
+
+class ValidationPanel extends JPanel {
+	private static final long serialVersionUID = -1585778734201458665L;
+	
+	private static final String[] COLUMN_NAMES = {
+	    tr("Description"), tr("Type"), tr("Quick-Fix")
+	};
+	
+	private final Action refreshAction = new JosmAction(tr("Refresh"), "dialogs/refresh",
+	    tr("Revalidate all turnlanes-relations."), null, false) {
+		private static final long serialVersionUID = -8110599654128234810L;
+		
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			setIssues(new Validator().validate(Main.main.getCurrentDataSet()));
+		}
+	};
+	
+	private final Action fixAction = new JosmAction(tr("Fix"), "dialogs/fix", tr("Automatically fixes the issue."), null,
+	    false) {
+		private static final long serialVersionUID = -8110599654128234810L;
+		
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			if (selected.getQuickFix().perform()) {
+				final int i = issues.indexOf(selected);
+				issueModel.removeRow(i);
+				issues.remove(i);
+			}
+		}
+	};
+	
+	private final Action selectAction = new JosmAction(tr("Select"), "dialogs/select",
+	    tr("Selects the offending relation."), null, false) {
+		private static final long serialVersionUID = -8110599654128234810L;
+		
+		@Override
+		public void actionPerformed(ActionEvent e) {
+			if (selected.getRelation() == null) {
+				Main.main.getCurrentDataSet().setSelected(selected.getPrimitives());
+			} else {
+				Main.main.getCurrentDataSet().setSelected(selected.getRelation());
+			}
+		}
+	};
+	
+	private final SideButton refreshButton = new SideButton(refreshAction);
+	private final SideButton fixButton = new SideButton(fixAction);
+	private final SideButton selectButton = new SideButton(selectAction);
+	
+	private final DefaultTableModel issueModel = new DefaultTableModel(COLUMN_NAMES, 0);
+	private final List<Issue> issues = new ArrayList<Issue>();
+	private final JTable issueTable = new JTable(issueModel) {
+		private static final long serialVersionUID = 6323348290180585298L;
+		
+		public boolean isCellEditable(int row, int column) {
+			return false;
+		};
+	};
+	
+	private Issue selected;
+	
+	public ValidationPanel() {
+		super(new BorderLayout(4, 4));
+		
+		final JPanel buttonPanel = new JPanel(new GridLayout(1, 3, 4, 4));
+		
+		buttonPanel.add(refreshButton);
+		buttonPanel.add(fixButton);
+		buttonPanel.add(selectButton);
+		
+		add(buttonPanel, BorderLayout.NORTH);
+		add(new JScrollPane(issueTable), BorderLayout.CENTER);
+		
+		issueTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+		issueTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+			@Override
+			public void valueChanged(ListSelectionEvent e) {
+				final int i = issueTable.getSelectedRow();
+				final Issue issue = i >= 0 ? issues.get(i) : null;
+				
+				setSelected(issue);
+			}
+		});
+		
+		setSelected(null);
+	}
+	
+	private void setIssues(List<Issue> issues) {
+		issueModel.setRowCount(0);
+		this.issues.clear();
+		
+		for (Issue i : issues) {
+			final String[] row = {
+			    i.getDescription(), //
+			    i.getRelation() == null ? tr("(none)") : i.getRelation().get("type"), //
+			    i.getQuickFix().getDescription()
+			};
+			issueModel.addRow(row);
+			this.issues.add(i);
+		}
+	}
+	
+	private void setSelected(Issue selected) {
+		this.selected = selected;
+		
+		if (selected == null) {
+			fixButton.setEnabled(false);
+			selectButton.setEnabled(false);
+		} else {
+			fixButton.setEnabled(selected.getQuickFix() != Issue.QuickFix.NONE);
+			selectButton.setEnabled(true);
+		}
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Constants.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Constants.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Constants.java	(revision 25606)
@@ -0,0 +1,26 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import java.util.regex.Pattern;
+
+public interface Constants {
+	String SEPARATOR = ";";
+	String SPLIT_REGEX = "[,:;]";
+	Pattern SPLIT_PATTERN = Pattern.compile(SPLIT_REGEX);
+	
+	String TYPE_LENGTHS = "turnlanes:lengths";
+	
+	String LENGTHS_KEY_LENGTHS_LEFT = "lengths:left";
+	String LENGTHS_KEY_LENGTHS_RIGHT = "lengths:right";
+	
+	String TYPE_TURNS = "turnlanes:turns";
+	
+	String TURN_ROLE_VIA = "via";
+	String TURN_ROLE_FROM = "from";
+	String TURN_ROLE_TO = "to";
+	
+	String TURN_KEY_LANES = "lanes";
+	String TURN_KEY_EXTRA_LANES = "lanes:extra";
+	String LENGTHS_ROLE_END = "end";
+	String LENGTHS_ROLE_WAYS = "ways";
+	
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Issue.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Issue.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Issue.java	(revision 25606)
@@ -0,0 +1,97 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+
+public class Issue {
+	public enum Severity {
+		INFO,
+		WARN,
+		ERROR;
+	}
+	
+	public static abstract class QuickFix {
+		public static final QuickFix NONE = new QuickFix(tr("None")) {
+			
+			@Override
+			public boolean perform() {
+				throw new UnsupportedOperationException("Don't call perform on Issue.QuickFix.NONE.");
+			}
+		};
+		
+		private final String description;
+		
+		public QuickFix(String description) {
+			this.description = description;
+		}
+		
+		public String getDescription() {
+			return description;
+		}
+		
+		public abstract boolean perform();
+	}
+	
+	private final Severity severity;
+	private final Relation relation;
+	private final List<OsmPrimitive> primitives;
+	private final String description;
+	private final QuickFix quickFix;
+	
+	private Issue(Severity severity, Relation relation, List<? extends OsmPrimitive> primitives, String description,
+	    QuickFix quickFix) {
+		this.relation = relation;
+		this.primitives = Collections.unmodifiableList(new ArrayList<OsmPrimitive>(primitives));
+		this.severity = severity;
+		this.description = description;
+		this.quickFix = quickFix;
+	}
+	
+	public static Issue newError(Relation relation, List<? extends OsmPrimitive> primitives, String description,
+	    QuickFix quickFix) {
+		return new Issue(Severity.ERROR, relation, primitives, description, quickFix);
+	}
+	
+	public static Issue newError(Relation relation, List<? extends OsmPrimitive> primitives, String description) {
+		return newError(relation, primitives, description, QuickFix.NONE);
+	}
+	
+	public static Issue newError(Relation relation, OsmPrimitive primitive, String description) {
+		return newError(relation, Arrays.asList(primitive), description, QuickFix.NONE);
+	}
+	
+	public static Issue newError(Relation relation, String description) {
+		return newError(relation, Collections.<OsmPrimitive> emptyList(), description, QuickFix.NONE);
+	}
+	
+	public static Issue newWarning(List<OsmPrimitive> primitives, String description) {
+		return new Issue(Severity.WARN, null, primitives, description, QuickFix.NONE);
+	}
+	
+	public Severity getSeverity() {
+		return severity;
+	}
+	
+	public String getDescription() {
+		return description;
+	}
+	
+	public Relation getRelation() {
+		return relation;
+	}
+	
+	public List<OsmPrimitive> getPrimitives() {
+		return primitives;
+	}
+	
+	public QuickFix getQuickFix() {
+		return quickFix;
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Junction.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Junction.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Junction.java	(revision 25606)
@@ -0,0 +1,153 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class Junction {
+	private static final List<Way> filterHighways(List<OsmPrimitive> of) {
+		final List<Way> result = new ArrayList<Way>();
+		
+		for (OsmPrimitive p : of) {
+			if (p.getType() == OsmPrimitiveType.WAY && Utils.isRoad((Way) p)) {
+				result.add((Way) p);
+			}
+		}
+		
+		return result;
+	}
+	
+	private static List<Way> filterBeginsOrEndsAt(List<Way> ways, Node node) {
+		final List<Way> result = new ArrayList<Way>();
+		
+		for (Way w : ways) {
+			if (w.isFirstLastNode(node)) {
+				result.add(w);
+			}
+		}
+		
+		return result;
+	}
+	
+	private static List<Road> loadRoads(ModelContainer container, Junction j) {
+		final List<Way> ways = filterBeginsOrEndsAt(filterHighways(j.getNode().getReferrers()), j.getNode());
+		
+		return Road.map(container, ways, j);
+	}
+	
+	private final ModelContainer container;
+	
+	private final Node node;
+	private final List<Road> roads = new ArrayList<Road>();
+	
+	Junction(ModelContainer container, Node n) {
+		this.container = container;
+		this.node = n;
+		
+		container.register(this);
+		
+		if (isPrimary()) {
+			loadRoads(container, this);
+			// if turn data is invalid, this will force an exception now, not later during painting
+			getTurns();
+		}
+	}
+	
+	boolean isPrimary() {
+		return container.getPrimary().equals(this);
+	}
+	
+	public Node getNode() {
+		return node;
+	}
+	
+	public List<Road> getRoads() {
+		return roads;
+	}
+	
+	void addRoad(Road r) {
+		roads.add(r);
+	}
+	
+	public void addTurn(Lane from, Road.End to) {
+		assert equals(from.getOutgoingJunction());
+		assert equals(to.getJunction());
+		
+		final Way fromWay = from.isReverse() ? from.getRoad().getRoute().getFirstSegment().getWay() : from.getRoad()
+		    .getRoute().getLastSegment().getWay();
+		final Way toWay = to.isFromEnd() ? to.getRoad().getRoute().getFirstSegment().getWay() : to.getRoad().getRoute()
+		    .getLastSegment().getWay();
+		
+		Relation existing = null;
+		for (Turn t : getTurns()) {
+			if ((from.isReverse() ? from.getRoad().getRoute().getFirstSegment() : from.getRoad().getRoute().getLastSegment())
+			    .getWay().equals(t.getFromWay()) && t.getTo().equals(to)) {
+				if (t.getFrom().isExtra() == from.isExtra() && t.getFrom().getIndex() == from.getIndex()) {
+					// was already added
+					return;
+				}
+				
+				existing = t.getRelation();
+			}
+		}
+		
+		final Relation r = existing == null ? new Relation() : existing;
+		
+		final String key = from.isExtra() ? Constants.TURN_KEY_EXTRA_LANES : Constants.TURN_KEY_LANES;
+		final List<Integer> lanes = Turn.split(r.get(key));
+		lanes.add(from.getIndex());
+		r.put(key, Turn.join(lanes));
+		
+		if (existing == null) {
+			r.put("type", Constants.TYPE_TURNS);
+			
+			r.addMember(new RelationMember(Constants.TURN_ROLE_VIA, node));
+			r.addMember(new RelationMember(Constants.TURN_ROLE_FROM, fromWay));
+			r.addMember(new RelationMember(Constants.TURN_ROLE_TO, toWay));
+			
+			node.getDataSet().addPrimitive(r);
+		}
+	}
+	
+	public Set<Turn> getTurns() {
+		return Turn.load(this);
+	}
+	
+	Road.End getRoadEnd(Way way) {
+		final List<Road.End> candidates = new ArrayList<Road.End>();
+		
+		for (Road r : getRoads()) {
+			if (r.getFromEnd().getJunction().equals(this)) {
+				if (r.getRoute().getSegments().get(0).getWay().equals(way)) {
+					candidates.add(r.getFromEnd());
+				}
+			}
+			
+			if (r.getToEnd().getJunction().equals(this)) {
+				if (r.getRoute().getSegments().get(r.getRoute().getSegments().size() - 1).getWay().equals(way)) {
+					candidates.add(r.getToEnd());
+				}
+			}
+		}
+		
+		if (candidates.isEmpty()) {
+			throw new IllegalArgumentException("No such road end.");
+		} else if (candidates.size() > 1) {
+			throw new IllegalArgumentException("There are " + candidates.size()
+			    + " road ends at this junction for the given way.");
+		}
+		
+		return candidates.get(0);
+	}
+	
+	public void removeTurn(Turn turn) {
+		turn.remove();
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Lane.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Lane.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Lane.java	(revision 25606)
@@ -0,0 +1,235 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.plugins.turnlanes.CollectionUtils;
+
+public class Lane {
+	public enum Kind {
+		EXTRA_LEFT,
+		EXTRA_RIGHT,
+		REGULAR;
+	}
+	
+	/**
+	 * 
+	 * @param road
+	 * @param r
+	 *          lengths relation
+	 * @param w
+	 * @return
+	 */
+	public static List<Lane> load(Road road, Relation left, Relation right, Way w) {
+		double extraLengthLeft = left == null ? 0 : Route.load(left).getLengthFrom(w);
+		double extraLengthRight = right == null ? 0 : Route.load(right).getLengthFrom(w);
+		
+		final List<Double> leftLengths = loadLengths(left, Constants.LENGTHS_KEY_LENGTHS_LEFT, extraLengthLeft);
+		final List<Double> rightLengths = loadLengths(right, Constants.LENGTHS_KEY_LENGTHS_RIGHT, extraLengthRight);
+		
+		final List<Lane> result = new ArrayList<Lane>(load(road));
+		final int mid = getReverseCount(result);
+		result.addAll(mid, map(leftLengths, road, true, extraLengthLeft));
+		result.addAll(map(rightLengths, road, false, extraLengthRight));
+		
+		return result;
+	}
+	
+	private static List<Lane> map(List<Double> lengths, Road road, boolean left, double extraLength) {
+		final List<Lane> result = new ArrayList<Lane>(lengths.size());
+		
+		int index = left ? -lengths.size() : 1;
+		for (Double l : (left ? CollectionUtils.reverse(lengths) : lengths)) {
+			// TODO road may connect twice with junction => reverse might not always be false
+			result.add(new Lane(road, index++, false, left, extraLength, l - extraLength));
+		}
+		
+		return result;
+	}
+	
+	private static int getReverseCount(List<Lane> ls) {
+		int result = 0;
+		
+		for (Lane l : ls) {
+			if (l.isReverse()) {
+				++result;
+			}
+		}
+		
+		return result;
+	}
+	
+	static List<Double> loadLengths(Relation r, String key, double lengthBound) {
+		final List<Double> result = new ArrayList<Double>();
+		
+		if (r != null && r.get(key) != null) {
+			for (String s : Constants.SPLIT_PATTERN.split(r.get(key))) {
+				// TODO what should the exact input be (there should probably be
+				// a unit (m))
+				final Double length = Double.parseDouble(s.trim());
+				
+				if (length > lengthBound) {
+					result.add(length);
+				}
+			}
+		}
+		
+		return result;
+	}
+	
+	private static int getCount(Way w) {
+		final String countStr = w.get("lanes");
+		
+		if (countStr != null) {
+			return Integer.parseInt(countStr);
+		}
+		
+		// TODO default lane counts based on "highway" tag
+		return 2;
+	}
+	
+	static int getRegularCount(Way w, Node end) {
+		final int count = getCount(w);
+		
+		if (w.hasDirectionKeys()) {
+			// TODO check for oneway=-1
+			if (w.lastNode().equals(end)) {
+				return count;
+			} else {
+				return 0;
+			}
+		} else {
+			if (w.lastNode().equals(end)) {
+				return (count + 1) / 2; // round up in direction of end
+			} else {
+				return count / 2; // round down in other direction
+			}
+		}
+	}
+	
+	public static List<Lane> load(Road r) {
+		final Route.Segment first = r.getRoute().getFirstSegment();
+		final Route.Segment last = r.getRoute().getLastSegment();
+		
+		final int back = getRegularCount(first.getWay(), first.getStart());
+		final int forth = getRegularCount(last.getWay(), last.getEnd());
+		
+		final List<Lane> result = new ArrayList<Lane>(back + forth);
+		
+		for (int i = back; i >= 1; --i) {
+			result.add(new Lane(r, i, true));
+		}
+		for (int i = 1; i <= forth; ++i) {
+			result.add(new Lane(r, i, false));
+		}
+		
+		return Collections.unmodifiableList(result);
+	}
+	
+	private final Road road;
+	private final int index;
+	private final Kind kind;
+	private final boolean reverse;
+	
+	/**
+	 * If this extra lane extends past the given road, this value equals the length of the way(s)
+	 * which come after the end of the road.
+	 */
+	private final double extraLength;
+	
+	private double length = -1;
+	
+	public Lane(Road road, int index, boolean reverse) {
+		this.road = road;
+		this.index = index;
+		this.kind = Kind.REGULAR;
+		this.reverse = reverse;
+		this.extraLength = -1;
+	}
+	
+	public Lane(Road road, int index, boolean reverse, boolean left, double extraLength, double length) {
+		this.road = road;
+		this.index = index;
+		this.kind = left ? Kind.EXTRA_LEFT : Kind.EXTRA_RIGHT;
+		this.reverse = reverse;
+		this.extraLength = extraLength;
+		this.length = length;
+		
+		if (length <= 0) {
+			throw new IllegalArgumentException("Length must be positive");
+		}
+		
+		if (extraLength < 0) {
+			throw new IllegalArgumentException("Extra length must not be negative");
+		}
+	}
+	
+	public Road getRoad() {
+		return road;
+	}
+	
+	public double getExtraLength() {
+		return extraLength;
+	}
+	
+	public Kind getKind() {
+		return kind;
+	}
+	
+	public double getLength() {
+		return isExtra() ? length : road.getLength();
+	}
+	
+	double getTotalLength() {
+		if (!isExtra()) {
+			throw new UnsupportedOperationException();
+		}
+		
+		return length + extraLength;
+	}
+	
+	public void setLength(double length) {
+		if (!isExtra()) {
+			throw new UnsupportedOperationException("Length can only be set for extra lanes.");
+		} else if (length <= 0) {
+			throw new IllegalArgumentException("Length must positive.");
+		}
+		
+		// TODO if needed, increase length of other lanes
+		road.updateLengths();
+		
+		this.length = length;
+	}
+	
+	public boolean isExtra() {
+		return getKind() != Kind.REGULAR;
+	}
+	
+	public int getIndex() {
+		return index;
+	}
+	
+	public boolean isReverse() {
+		return reverse;
+	}
+	
+	public Junction getOutgoingJunction() {
+		return isReverse() ? getRoad().getFromEnd().getJunction() : getRoad().getToEnd().getJunction();
+	}
+	
+	public Junction getIncomingJunction() {
+		return isReverse() ? getRoad().getToEnd().getJunction() : getRoad().getFromEnd().getJunction();
+	}
+	
+	public Road.End getOutgoingRoadEnd() {
+		return isReverse() ? getRoad().getFromEnd() : getRoad().getToEnd();
+	}
+	
+	public Road.End getIncomingRoadEnd() {
+		return isReverse() ? getRoad().getToEnd() : getRoad().getFromEnd();
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/ModelContainer.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/ModelContainer.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/ModelContainer.java	(revision 25606)
@@ -0,0 +1,78 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class ModelContainer {
+	public static ModelContainer create(Node n) {
+		final ModelContainer container = new ModelContainer(n);
+		container.getOrCreateJunction(n);
+		return container;
+	}
+	
+	private final Map<Node, Junction> junctions = new HashMap<Node, Junction>();
+	private final Map<Way, Road> roads = new HashMap<Way, Road>();
+	
+	private final Node primary;
+	
+	private ModelContainer(Node primary) {
+		this.primary = primary;
+	}
+	
+	Junction getOrCreateJunction(Node n) {
+		final Junction existing = junctions.get(n);
+		
+		if (existing != null) {
+			return existing;
+		}
+		
+		return new Junction(this, n);
+	}
+	
+	public Junction getJunction(Node n) {
+		Junction j = junctions.get(n);
+		
+		if (j == null) {
+			throw new IllegalArgumentException();
+		}
+		
+		return j;
+	}
+	
+	Road getRoad(Way w, Junction j) {
+		final Road existing = roads.get(w);
+		
+		if (existing != null && j.equals(existing.getToEnd().getJunction())) {
+			return existing;
+		}
+		
+		final Road newRoad = new Road(this, w, j);
+		
+		for (Route.Segment s : newRoad.getRoute().getSegments()) {
+			final Road oldRoad = roads.put(s.getWay(), newRoad);
+			
+			if (oldRoad != null) {
+				return mergeRoads(oldRoad, newRoad);
+			}
+		}
+		
+		return newRoad;
+	}
+	
+	private Road mergeRoads(Road a, Road b) {
+		throw null; // TODO implement
+	}
+	
+	void register(Junction j) {
+		if (junctions.put(j.getNode(), j) != null) {
+			throw new IllegalStateException();
+		}
+	}
+	
+	Junction getPrimary() {
+		return junctions.get(primary);
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Road.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Road.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Road.java	(revision 25606)
@@ -0,0 +1,322 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.plugins.turnlanes.model.Route.Segment;
+import org.openstreetmap.josm.tools.Pair;
+
+public class Road {
+	public class End {
+		private final Junction junction;
+		
+		private End(Junction junction) {
+			this.junction = junction;
+		}
+		
+		public Road getRoad() {
+			return Road.this;
+		}
+		
+		public Junction getJunction() {
+			return junction;
+		}
+		
+		private boolean isFrom() {
+			if (this == fromEnd) {
+				return true;
+			} else if (this == toEnd) {
+				return false;
+			}
+			
+			throw new IllegalStateException();
+		}
+		
+		public boolean isFromEnd() {
+			return isFrom();
+		}
+		
+		public boolean isToEnd() {
+			return !isFrom();
+		}
+		
+		public void addExtraLane(boolean left) {
+			if (!isToEnd()) {
+				throw new UnsupportedOperationException();
+			}
+			
+			double length = Double.POSITIVE_INFINITY;
+			for (Lane l : getLanes()) {
+				if (l.getKind() == (left ? Lane.Kind.EXTRA_LEFT : Lane.Kind.EXTRA_RIGHT)) {
+					{
+						length = Math.min(length, l.getLength());
+					}
+				}
+			}
+			
+			if (Double.isInfinite(length)) {
+				length = Math.min(20, 3 * getLength() / 4);
+			}
+			
+			addLength(left, length);
+		}
+	}
+	
+	public static List<Road> map(ModelContainer container, List<Way> ws, Junction j) {
+		final List<Road> result = new ArrayList<Road>();
+		
+		for (Way w : ws) {
+			result.add(container.getRoad(w, j));
+		}
+		
+		return result;
+	}
+	
+	private static Pair<Relation, Relation> getLengthRelations(Way w, Node n) {
+		final List<Relation> left = new ArrayList<Relation>();
+		final List<Relation> right = new ArrayList<Relation>();
+		
+		for (OsmPrimitive p : w.getReferrers()) {
+			if (p.getType() != OsmPrimitiveType.RELATION) {
+				continue;
+			}
+			
+			Relation r = (Relation) p;
+			
+			if (Constants.TYPE_LENGTHS.equals(r.get("type")) && isRightDirection(r, w, n)) {
+				
+				if (r.get(Constants.LENGTHS_KEY_LENGTHS_LEFT) != null) {
+					left.add(r);
+				}
+				
+				if (r.get(Constants.LENGTHS_KEY_LENGTHS_RIGHT) != null) {
+					right.add(r);
+				}
+			}
+		}
+		
+		if (left.size() > 1) {
+			throw new IllegalArgumentException("Way is in " + left.size()
+			    + " lengths relations for given direction, both specifying left lane lengths.");
+		}
+		
+		if (right.size() > 1) {
+			throw new IllegalArgumentException("Way is in " + right.size()
+			    + " lengths relations for given direction, both specifying right lane lengths.");
+		}
+		
+		return new Pair<Relation, Relation>( //
+		    left.isEmpty() ? null : left.get(0), //
+		    right.isEmpty() ? null : right.get(0) //
+		);
+	}
+	
+	/**
+	 * @param r
+	 *          lengths relation
+	 * @param w
+	 *          the way to check for
+	 * @param n
+	 *          first or last node of w, determines the direction
+	 * @return whether the turn lane goes into the direction of n
+	 */
+	private static boolean isRightDirection(Relation r, Way w, Node n) {
+		for (Segment s : Route.load(r).getSegments()) {
+			if (w.equals(s.getWay())) {
+				return n.equals(s.getEnd());
+			}
+		}
+		
+		return false;
+	}
+	
+	private final ModelContainer container;
+	
+	private final Relation lengthsLeft;
+	private final Relation lengthsRight;
+	private final Route route;
+	private final List<Lane> lanes;
+	
+	private final End fromEnd;
+	private final End toEnd;
+	
+	Road(ModelContainer container, Way w, Junction j) {
+		this.container = container;
+		this.toEnd = new End(j);
+		
+		final Node n = j.getNode();
+		
+		if (!w.isFirstLastNode(n)) {
+			throw new IllegalArgumentException("Way must start or end in given node.");
+		}
+		
+		final Pair<Relation, Relation> lr = getLengthRelations(w, n);
+		
+		this.lengthsLeft = lr.a;
+		this.lengthsRight = lr.b;
+		this.route = lengthsLeft == null && lengthsRight == null ? Route.create(Arrays.asList(w), n) : Route.load(
+		    lengthsLeft, lengthsRight, w);
+		this.lanes = lengthsLeft == null && lengthsRight == null ? Lane.load(this) : Lane.load(this, lengthsLeft,
+		    lengthsRight, w);
+		
+		final List<Node> nodes = route.getNodes();
+		this.fromEnd = new End(container.getOrCreateJunction(nodes.get(0)));
+		
+		fromEnd.getJunction().addRoad(this);
+		toEnd.getJunction().addRoad(this);
+	}
+	
+	public End getFromEnd() {
+		return fromEnd;
+	}
+	
+	public End getToEnd() {
+		return toEnd;
+	}
+	
+	public Route getRoute() {
+		return route;
+	}
+	
+	public List<Lane> getLanes() {
+		return lanes;
+	}
+	
+	public double getLength() {
+		return route.getLength();
+	}
+	
+	void updateLengths() {
+		if (lengthsLeft != null) { // TODO only forward lanes at this point
+			String lengths = "";
+			for (int i = lanes.size() - 1; i >= 0; --i) {
+				final Lane l = lanes.get(i);
+				if (l.getKind() == Lane.Kind.EXTRA_LEFT) {
+					lengths += toLengthString(l.getTotalLength()) + Constants.SEPARATOR;
+				}
+			}
+			lengthsLeft.put(Constants.LENGTHS_KEY_LENGTHS_LEFT,
+			    lengths.substring(0, lengths.length() - Constants.SEPARATOR.length()));
+		}
+		if (lengthsRight != null) {
+			String lengths = "";
+			for (Lane l : lanes) {
+				if (l.getKind() == Lane.Kind.EXTRA_RIGHT) {
+					lengths += toLengthString(l.getTotalLength()) + Constants.SEPARATOR;
+				}
+			}
+			lengthsRight.put(Constants.LENGTHS_KEY_LENGTHS_RIGHT,
+			    lengths.substring(0, lengths.length() - Constants.SEPARATOR.length()));
+		}
+	}
+	
+	private String toLengthString(double length) {
+		final DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance();
+		dfs.setDecimalSeparator('.');
+		final DecimalFormat nf = new DecimalFormat("0.0", dfs);
+		nf.setRoundingMode(RoundingMode.HALF_UP);
+		return nf.format(length);
+	}
+	
+	// TODO create a special Lanes class which deals with this, misplaced here
+	public Lane getLane(boolean reverse, int index) {
+		for (Lane l : lanes) {
+			if (!l.isExtra() && l.isReverse() == reverse && l.getIndex() == index) {
+				return l;
+			}
+		}
+		
+		throw new IllegalArgumentException("No such lane.");
+	}
+	
+	public Lane getExtraLane(boolean reverse, int index) {
+		for (Lane l : lanes) {
+			if (l.isExtra() && l.isReverse() == reverse && l.getIndex() == index) {
+				return l;
+			}
+		}
+		
+		throw new IllegalArgumentException("No such lane.");
+	}
+	
+	public ModelContainer getContainer() {
+		return container;
+	}
+	
+	private void addLength(boolean left, double length) {
+		final Relation l = lengthsLeft;
+		final Relation r = lengthsRight;
+		final Node n = toEnd.getJunction().getNode();
+		
+		final String lengthStr = toLengthString(length);
+		final Relation target;
+		if (left) {
+			if (l == null) {
+				if (r == null || !Utils.getMemberNode(r, "end").equals(n)) {
+					target = createLengthsRelation();
+				} else {
+					target = r;
+				}
+			} else {
+				target = l;
+			}
+		} else {
+			if (r == null) {
+				if (l == null || !Utils.getMemberNode(l, "end").equals(n)) {
+					target = createLengthsRelation();
+				} else {
+					target = l;
+				}
+			} else {
+				target = r;
+			}
+		}
+		
+		final String key = left ? Constants.LENGTHS_KEY_LENGTHS_LEFT : Constants.LENGTHS_KEY_LENGTHS_RIGHT;
+		final String old = target.get(key);
+		if (old == null) {
+			target.put(key, lengthStr);
+		} else {
+			target.put(key, old + Constants.SEPARATOR + lengthStr);
+		}
+	}
+	
+	private Relation createLengthsRelation() {
+		final Node n = toEnd.getJunction().getNode();
+		
+		final Relation r = new Relation();
+		r.put("type", Constants.TYPE_LENGTHS);
+		
+		r.addMember(new RelationMember(Constants.LENGTHS_ROLE_END, n));
+		for (Route.Segment s : route.getSegments()) {
+			r.addMember(1, new RelationMember(Constants.LENGTHS_ROLE_WAYS, s.getWay()));
+		}
+		
+		n.getDataSet().addPrimitive(r);
+		
+		return r;
+	}
+	
+	public boolean isExtendable() {
+		return lengthsLeft != null || lengthsRight != null;
+	}
+	
+	public void extend(Way way) {
+		if (lengthsLeft != null) {
+			lengthsLeft.addMember(new RelationMember(Constants.LENGTHS_ROLE_WAYS, way));
+		}
+		if (lengthsRight != null) {
+			lengthsRight.addMember(new RelationMember(Constants.LENGTHS_ROLE_WAYS, way));
+		}
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Route.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Route.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Route.java	(revision 25606)
@@ -0,0 +1,212 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class Route {
+	public static final class Segment {
+		private final Node start;
+		private final Way way;
+		private final Node end;
+		
+		private final List<Node> nodes;
+		
+		Segment(Node start, Way way, Node end) {
+			this.start = start;
+			this.way = way;
+			this.end = end;
+			
+			final List<Node> ns = way.getNodes();
+			if (way.lastNode().equals(start)) {
+				Collections.reverse(ns);
+			}
+			
+			this.nodes = Collections.unmodifiableList(ns);
+		}
+		
+		public Node getStart() {
+			return start;
+		}
+		
+		public Way getWay() {
+			return way;
+		}
+		
+		public Node getEnd() {
+			return end;
+		}
+		
+		public List<Node> getNodes() {
+			return nodes;
+		}
+		
+		public double getLength() {
+			double length = 0;
+			
+			Node last = nodes.get(0);
+			for (Node n : nodes.subList(1, nodes.size())) {
+				length += last.getCoor().greatCircleDistance(n.getCoor());
+				last = n;
+			}
+			
+			return length;
+		}
+		
+		@Override
+		public int hashCode() {
+			final int prime = 31;
+			int result = 1;
+			result = prime * result + ((end == null) ? 0 : end.hashCode());
+			result = prime * result + ((start == null) ? 0 : start.hashCode());
+			result = prime * result + ((way == null) ? 0 : way.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;
+			Segment other = (Segment) obj;
+			if (end == null) {
+				if (other.end != null)
+					return false;
+			} else if (!end.equals(other.end))
+				return false;
+			if (start == null) {
+				if (other.start != null)
+					return false;
+			} else if (!start.equals(other.start))
+				return false;
+			if (way == null) {
+				if (other.way != null)
+					return false;
+			} else if (!way.equals(other.way))
+				return false;
+			return true;
+		}
+	}
+	
+	public static Route load(Relation r) {
+		final Node end = Utils.getMemberNode(r, Constants.LENGTHS_ROLE_END);
+		final List<Way> ws = Utils.getMemberWays(r, Constants.LENGTHS_ROLE_WAYS);
+		
+		return create(ws, end);
+	}
+	
+	public static Route load(Relation left, Relation right, Way w) {
+		left = left == null ? right : left;
+		right = right == null ? left : right;
+		
+		if (left == null) {
+			throw new IllegalArgumentException("At least one relation must not be null.");
+		}
+		
+		final Route leftRoute = load(left);
+		final Route rightRoute = load(right);
+		
+		int iLeft = 0;
+		while (!w.equals(leftRoute.getSegments().get(iLeft++).getWay()))
+			;
+		
+		int iRight = 0;
+		while (!w.equals(rightRoute.getSegments().get(iRight++).getWay()))
+			;
+		
+		final int min = Math.min(iLeft, iRight);
+		
+		final List<Segment> leftSegments = leftRoute.getSegments().subList(iLeft - min, iLeft);
+		final List<Segment> rightSegments = rightRoute.getSegments().subList(iRight - min, iRight);
+		
+		if (!leftSegments.equals(rightSegments)) {
+			throw new IllegalArgumentException("Routes are split across different ways.");
+		}
+		
+		return new Route(iLeft == min ? rightSegments : leftSegments);
+	}
+	
+	public static Route create(List<Way> ws, Node end) {
+		final List<Segment> segments = new ArrayList<Segment>(ws.size());
+		
+		for (Way w : ws) {
+			if (!w.isFirstLastNode(end)) {
+				throw new IllegalArgumentException("Ways must be ordered.");
+			}
+			
+			final Node start = Utils.getOppositeEnd(w, end);
+			segments.add(0, new Segment(start, w, end));
+			end = start;
+		}
+		
+		return new Route(segments);
+	}
+	
+	private final List<Segment> segments;
+	
+	private Route(List<Segment> segments) {
+		this.segments = Collections.unmodifiableList(new ArrayList<Segment>(segments));
+	}
+	
+	public List<Segment> getSegments() {
+		return segments;
+	}
+	
+	public List<Node> getNodes() {
+		final List<Node> ns = new ArrayList<Node>();
+		
+		ns.add(segments.get(0).getStart());
+		for (Segment s : segments) {
+			ns.addAll(s.getNodes().subList(1, s.getNodes().size()));
+		}
+		
+		return Collections.unmodifiableList(ns);
+	}
+	
+	public double getLengthFrom(Way w) {
+		double length = Double.NEGATIVE_INFINITY;
+		
+		for (Segment s : getSegments()) {
+			length += s.getLength();
+			
+			if (w.equals(s.getWay())) {
+				length = 0;
+			}
+		}
+		
+		if (length < 0) {
+			throw new IllegalArgumentException("Way must be part of the route.");
+		}
+		
+		return length;
+	}
+	
+	public double getLength() {
+		double length = 0;
+		
+		for (Segment s : getSegments()) {
+			length += s.getLength();
+		}
+		
+		return length;
+	}
+	
+	public Segment getFirstSegment() {
+		return getSegments().get(0);
+	}
+	
+	public Segment getLastSegment() {
+		return getSegments().get(getSegments().size() - 1);
+	}
+	
+	public Route subRoute(int fromIndex, int toIndex) {
+		return new Route(segments.subList(fromIndex, toIndex));
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Turn.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Turn.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Turn.java	(revision 25606)
@@ -0,0 +1,198 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class Turn {
+	public static Set<Turn> load(Junction junction) {
+		final Set<Turn> result = new HashSet<Turn>();
+
+		for (OsmPrimitive p : junction.getNode().getReferrers()) {
+			if (p.getType() != OsmPrimitiveType.RELATION) {
+				continue;
+			}
+
+			Relation r = (Relation) p;
+
+			if (!r.get("type").equals(Constants.TYPE_TURNS)) {
+				continue;
+			}
+
+			result.addAll(Turn.load(junction, r));
+		}
+
+		return result;
+	}
+
+	private static Set<Turn> load(Junction junction, Relation turns) {
+		if (!Constants.TYPE_TURNS.equals(turns.get("type"))) {
+			throw new IllegalArgumentException("Relation must be a of type \""
+					+ Constants.TYPE_TURNS + "\".");
+		}
+
+		Way fromWay = null;
+		Way toWay = null;
+		Node via = null;
+		final List<Integer> lanes = split(turns.get(Constants.TURN_KEY_LANES));
+		final List<Integer> extraLanes = split(turns
+				.get(Constants.TURN_KEY_EXTRA_LANES));
+
+		for (RelationMember m : turns.getMembers()) {
+			final String r = m.getRole();
+			if (Constants.TURN_ROLE_VIA.equals(r)) {
+				if (via != null) {
+					throw new IllegalArgumentException("More than one \""
+							+ Constants.TURN_ROLE_VIA + "\" members.");
+				}
+
+				via = m.getNode();
+			} else if (Constants.TURN_ROLE_FROM.equals(r)) {
+				if (fromWay != null) {
+					throw new IllegalArgumentException("More than one \""
+							+ Constants.TURN_ROLE_FROM + "\" members.");
+				}
+
+				fromWay = m.getWay();
+			} else if (Constants.TURN_ROLE_TO.equals(r)) {
+				if (toWay != null) {
+					throw new IllegalArgumentException("More than one \""
+							+ Constants.TURN_ROLE_TO + "\" members.");
+				}
+
+				toWay = m.getWay();
+			}
+		}
+
+		if (!via.equals(junction.getNode())) {
+			throw new IllegalArgumentException(
+					"Turn is for a different junction.");
+		}
+
+		final Road.End to = junction.getRoadEnd(toWay);
+
+		final Set<Turn> result = new HashSet<Turn>();
+		for (Road r : junction.getRoads()) {
+			final boolean reverse;
+			if (r.getRoute().getFirstSegment().getWay().equals(fromWay)
+					&& r.getRoute().getFirstSegment().getStart().equals(via)) {
+				reverse = true;
+			} else if (r.getRoute().getLastSegment().getWay().equals(fromWay)
+					&& r.getRoute().getLastSegment().getEnd().equals(via)) {
+				reverse = false;
+			} else {
+				continue;
+			}
+
+			for (int l : lanes) {
+				result.add(new Turn(turns, r.getLane(reverse, l), junction, to,
+						fromWay, toWay));
+			}
+			for (int l : extraLanes) {
+				result.add(new Turn(turns, r.getExtraLane(reverse, l),
+						junction, to, fromWay, toWay));
+			}
+		}
+
+		return result;
+	}
+
+	static List<Integer> split(String lanes) {
+		if (lanes == null) {
+			return new ArrayList<Integer>(1);
+		}
+
+		final List<Integer> result = new ArrayList<Integer>();
+		for (String lane : Constants.SPLIT_PATTERN.split(lanes)) {
+			result.add(Integer.parseInt(lane));
+		}
+
+		return result;
+	}
+
+	static String join(List<Integer> list) {
+		if (list.isEmpty()) {
+			return null;
+		}
+
+		final StringBuilder builder = new StringBuilder(list.size()
+				* (2 + Constants.SEPARATOR.length()));
+
+		for (int e : list) {
+			builder.append(e).append(Constants.SEPARATOR);
+		}
+
+		builder.setLength(builder.length() - Constants.SEPARATOR.length());
+		return builder.toString();
+	}
+
+	private final Relation relation;
+
+	private final Lane from;
+	private final Junction via;
+	private final Road.End to;
+
+	private final Way fromWay;
+	private final Way toWay;
+
+	public Turn(Relation relation, Lane from, Junction via, Road.End to,
+			Way fromWay, Way toWay) {
+		this.relation = relation;
+		this.from = from;
+		this.via = via;
+		this.to = to;
+		this.fromWay = fromWay;
+		this.toWay = toWay;
+	}
+
+	public Lane getFrom() {
+		return from;
+	}
+
+	public Junction getVia() {
+		return via;
+	}
+
+	public Road.End getTo() {
+		return to;
+	}
+
+	Relation getRelation() {
+		return relation;
+	}
+
+	Way getFromWay() {
+		return fromWay;
+	}
+
+	Way getToWay() {
+		return toWay;
+	}
+
+	void remove() {
+		final List<Integer> lanes = split(relation
+				.get(Constants.TURN_KEY_LANES));
+		final List<Integer> extraLanes = split(relation
+				.get(Constants.TURN_KEY_EXTRA_LANES));
+
+		if (lanes.size() + extraLanes.size() == 1
+				&& (from.isExtra() ^ !lanes.isEmpty())) {
+			relation.getDataSet().removePrimitive(relation.getPrimitiveId());
+		} else if (from.isExtra()) {
+			extraLanes.remove(Integer.valueOf(from.getIndex()));
+		} else {
+			lanes.remove(Integer.valueOf(from.getIndex()));
+		}
+
+		relation.put(Constants.TURN_KEY_LANES, join(lanes));
+		relation.put(Constants.TURN_KEY_EXTRA_LANES, join(extraLanes));
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Utils.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Utils.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Utils.java	(revision 25606)
@@ -0,0 +1,88 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class Utils {
+	private static final Set<String> ROAD_HIGHWAY_VALUES = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
+	    "motorway", "motorway_link", "trunk", "trunk_link", "primary", "primary_link", "secondary", "secondary_link",
+	    "tertiary", "residential", "unclassified", "road", "living_street", "service", "track", "pedestrian", "raceway",
+	    "services")));
+	
+	public static boolean isRoad(Way w) {
+		return ROAD_HIGHWAY_VALUES.contains(w.get("highway"));
+	}
+	
+	public static Node getMemberNode(Relation r, String role) {
+		return getMember(r, role, OsmPrimitiveType.NODE).getNode();
+	}
+	
+	public static RelationMember getMember(Relation r, String role, OsmPrimitiveType type) {
+		final List<RelationMember> candidates = getMembers(r, role, type);
+		
+		if (candidates.size() == 0) {
+			throw new IllegalStateException("No member with given role and type.");
+		} else if (candidates.size() > 1) {
+			throw new IllegalStateException(candidates.size() + " members with given role and type.");
+		}
+		
+		return candidates.get(0);
+	}
+	
+	public static List<RelationMember> getMembers(Relation r, String role, OsmPrimitiveType type) {
+		final List<RelationMember> result = new ArrayList<RelationMember>();
+		
+		for (RelationMember m : r.getMembers()) {
+			if (m.getRole().equals(role) && m.getType() == type) {
+				result.add(m);
+			}
+		}
+		
+		return result;
+	}
+	
+	public static List<Way> getMemberWays(Relation r, String role) {
+		final List<Way> result = new ArrayList<Way>();
+		
+		for (RelationMember m : getMembers(r, role, OsmPrimitiveType.WAY)) {
+			result.add(m.getWay());
+		}
+		
+		return result;
+	}
+	
+	public static List<Node> getMemberNodes(Relation r, String role) {
+		final List<Node> result = new ArrayList<Node>();
+		
+		for (RelationMember m : getMembers(r, role, OsmPrimitiveType.NODE)) {
+			result.add(m.getNode());
+		}
+		
+		return result;
+	}
+	
+	public static Node getOppositeEnd(Way w, Node n) {
+		final boolean first = n.equals(w.firstNode());
+		final boolean last = n.equals(w.lastNode());
+		
+		if (first && last) {
+			throw new IllegalArgumentException("Way starts as well as ends at the given node.");
+		} else if (first) {
+			return w.lastNode();
+		} else if (last) {
+			return w.firstNode();
+		} else {
+			throw new IllegalArgumentException("Way neither starts nor ends at given node.");
+		}
+	}
+}
Index: applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Validator.java
===================================================================
--- applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Validator.java	(revision 25606)
+++ applications/editors/josm/plugins/turnlanes/src/org/openstreetmap/josm/plugins/turnlanes/model/Validator.java	(revision 25606)
@@ -0,0 +1,418 @@
+package org.openstreetmap.josm.plugins.turnlanes.model;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.plugins.turnlanes.model.Issue.QuickFix;
+
+public class Validator {
+	private static final class IncomingLanes {
+		private static final class Key {
+			final Node junction;
+			final Way from;
+			
+			public Key(Node junction, Way from) {
+				this.junction = junction;
+				this.from = from;
+			}
+			
+			@Override
+			public int hashCode() {
+				final int prime = 31;
+				int result = 1;
+				result = prime * result + ((from == null) ? 0 : from.hashCode());
+				result = prime * result + ((junction == null) ? 0 : junction.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;
+				Key other = (Key) obj;
+				if (from == null) {
+					if (other.from != null)
+						return false;
+				} else if (!from.equals(other.from))
+					return false;
+				if (junction == null) {
+					if (other.junction != null)
+						return false;
+				} else if (!junction.equals(other.junction))
+					return false;
+				return true;
+			}
+		}
+		
+		final Key key;
+		private final int extraLeft;
+		private final int regular;
+		private final int extraRight;
+		private final BitSet bitset;
+		
+		public IncomingLanes(Key key, int extraLeft, int regular, int extraRight) {
+			this.key = key;
+			this.extraLeft = extraLeft;
+			this.regular = regular;
+			this.extraRight = extraRight;
+			this.bitset = new BitSet(extraLeft + regular + extraRight);
+		}
+		
+		public boolean existsRegular(int l) {
+			if (l > 0 && l <= regular) {
+				bitset.set(extraLeft + l - 1);
+				return true;
+			}
+			
+			return false;
+		}
+		
+		public boolean existsExtra(int l) {
+			if (l < 0 && Math.abs(l) <= extraLeft) {
+				bitset.set(Math.abs(l) - 1);
+				return true;
+			} else if (l > 0 && l <= extraRight) {
+				bitset.set(extraLeft + regular + l - 1);
+				return true;
+			}
+			return false;
+		}
+		
+		public int unreferenced() {
+			return extraLeft + regular + extraRight - bitset.cardinality();
+		}
+	}
+	
+	public List<Issue> validate(DataSet dataSet) {
+		final List<Relation> lenghts = new ArrayList<Relation>();
+		final List<Relation> turns = new ArrayList<Relation>();
+		
+		for (OsmPrimitive p : dataSet.allPrimitives()) {
+			if (p.getType() != OsmPrimitiveType.RELATION) {
+				continue;
+			}
+			
+			final Relation r = (Relation) p;
+			final String type = p.get("type");
+			
+			if (Constants.TYPE_LENGTHS.equals(type)) {
+				lenghts.add(r);
+			} else if (Constants.TYPE_TURNS.equals(type)) {
+				turns.add(r);
+			}
+		}
+		
+		final List<Issue> issues = new ArrayList<Issue>();
+		
+		final Map<IncomingLanes.Key, IncomingLanes> incomingLanes = new HashMap<IncomingLanes.Key, IncomingLanes>();
+		issues.addAll(validateLengths(lenghts, incomingLanes));
+		issues.addAll(validateTurns(turns, incomingLanes));
+		
+		for (IncomingLanes lanes : incomingLanes.values()) {
+			if (lanes.unreferenced() > 0) {
+				issues.add(Issue.newWarning(Arrays.asList(lanes.key.junction, lanes.key.from),
+				    tr("{0} lanes are not referenced in any turn-relation.", lanes.unreferenced())));
+			}
+		}
+		
+		return issues;
+	}
+	
+	private List<Issue> validateLengths(List<Relation> lenghts, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) {
+		final List<Issue> issues = new ArrayList<Issue>();
+		
+		for (Relation r : lenghts) {
+			issues.addAll(validateLengths(r, incomingLanes));
+		}
+		
+		return issues;
+	}
+	
+	private List<Issue> validateLengths(Relation r, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) {
+		final List<Issue> issues = new ArrayList<Issue>();
+		
+		final Node end = validateLengthsEnd(r, issues);
+		
+		if (end == null) {
+			return issues;
+		}
+		
+		final Route route = validateLengthsWays(r, end, issues);
+		
+		if (route == null) {
+			return issues;
+		}
+		
+		final List<Double> left = Lane.loadLengths(r, Constants.LENGTHS_KEY_LENGTHS_LEFT, 0);
+		final List<Double> right = Lane.loadLengths(r, Constants.LENGTHS_KEY_LENGTHS_RIGHT, 0);
+		
+		int tooLong = 0;
+		for (Double l : left) {
+			if (l > route.getLength()) {
+				++tooLong;
+			}
+		}
+		for (Double l : right) {
+			if (l > route.getLength()) {
+				++tooLong;
+			}
+		}
+		
+		if (tooLong > 0) {
+			issues.add(Issue.newError(r, end, "The lengths-relation specifies " + tooLong
+			    + " extra-lanes which are longer than its ways."));
+		}
+		
+		putIncomingLanes(route, left, right, incomingLanes);
+		
+		return issues;
+	}
+	
+	private void putIncomingLanes(Route route, List<Double> left, List<Double> right,
+	    Map<IncomingLanes.Key, IncomingLanes> incomingLanes) {
+		final Node end = route.getLastSegment().getEnd();
+		final Way way = route.getLastSegment().getWay();
+		
+		final IncomingLanes.Key key = new IncomingLanes.Key(end, way);
+		final IncomingLanes lanes = new IncomingLanes(key, left.size(), Lane.getRegularCount(way, end), right.size());
+		final IncomingLanes old = incomingLanes.put(key, lanes);
+		
+		if (old != null) {
+			incomingLanes.put(
+			    key,
+			    new IncomingLanes(key, Math.max(lanes.extraLeft, old.extraLeft), Math.max(lanes.regular, old.regular), Math
+			        .max(lanes.extraRight, old.extraRight)));
+		}
+		
+		final double length = route.getLastSegment().getLength();
+		final List<Double> newLeft = reduceLengths(left, length);
+		final List<Double> newRight = new ArrayList<Double>(right.size());
+		
+		if (route.getSegments().size() > 1) {
+			final Route subroute = route.subRoute(0, route.getSegments().size() - 1);
+			putIncomingLanes(subroute, newLeft, newRight, incomingLanes);
+		}
+	}
+	
+	private List<Double> reduceLengths(List<Double> lengths, double length) {
+		final List<Double> newLengths = new ArrayList<Double>(lengths.size());
+		
+		for (double l : lengths) {
+			if (l > length) {
+				newLengths.add(l - length);
+			}
+		}
+		
+		return newLengths;
+	}
+	
+	private Route validateLengthsWays(Relation r, Node end, List<Issue> issues) {
+		final List<Way> ways = Utils.getMemberWays(r, Constants.LENGTHS_ROLE_WAYS);
+		
+		if (ways.isEmpty()) {
+			issues.add(Issue.newError(r, "A lengths-relation requires at least one member-way with role \""
+			    + Constants.LENGTHS_ROLE_WAYS + "\"."));
+			return null;
+		}
+		
+		Node current = end;
+		for (Way w : ways) {
+			if (!w.isFirstLastNode(current)) {
+				return orderWays(r, ways, current, issues);
+			}
+			
+			current = Utils.getOppositeEnd(w, current);
+		}
+		
+		return Route.create(ways, end);
+	}
+	
+	private Route orderWays(final Relation r, List<Way> ways, Node end, List<Issue> issues) {
+		final List<Way> unordered = new ArrayList<Way>(ways);
+		final List<Way> ordered = new ArrayList<Way>(ways.size());
+		final Set<Node> ends = new HashSet<Node>(); // to find cycles
+		
+		Node current = end;
+		findNext: while (!unordered.isEmpty()) {
+			if (!ends.add(current)) {
+				issues.add(Issue.newError(r, ways, "The ways of the lengths-relation are unordered (and contain cycles)."));
+				return null;
+			}
+			
+			Iterator<Way> it = unordered.iterator();
+			while (it.hasNext()) {
+				final Way w = it.next();
+				
+				if (w.isFirstLastNode(current)) {
+					it.remove();
+					ordered.add(w);
+					current = Utils.getOppositeEnd(w, current);
+					continue findNext;
+				}
+			}
+			
+			issues.add(Issue.newError(r, ways, "The ways of the lengths-relation are disconnected."));
+			return null;
+		}
+		
+		final QuickFix quickFix = new QuickFix(tr("Put the ways in order.")) {
+			@Override
+			public boolean perform() {
+				for (int i = r.getMembersCount() - 1; i >= 0; --i) {
+					final RelationMember m = r.getMember(i);
+					
+					if (m.isWay() && Constants.LENGTHS_ROLE_WAYS.equals(m.getRole())) {
+						r.removeMember(i);
+					}
+				}
+				
+				for (Way w : ordered) {
+					r.addMember(new RelationMember(Constants.LENGTHS_ROLE_WAYS, w));
+				}
+				
+				return true;
+			}
+		};
+		
+		issues.add(Issue.newError(r, ways, "The ways of the lengths-relation are unordered.", quickFix));
+		
+		return Route.create(ordered, end);
+	}
+	
+	private Node validateLengthsEnd(Relation r, List<Issue> issues) {
+		final List<Node> endNodes = Utils.getMemberNodes(r, Constants.LENGTHS_ROLE_END);
+		
+		if (endNodes.size() != 1) {
+			issues.add(Issue.newError(r, endNodes, "A lengths-relation requires exactly one member-node with role \""
+			    + Constants.LENGTHS_ROLE_END + "\"."));
+			return null;
+		}
+		
+		return endNodes.get(0);
+	}
+	
+	private List<Issue> validateTurns(List<Relation> turns, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) {
+		final List<Issue> issues = new ArrayList<Issue>();
+		
+		for (Relation r : turns) {
+			issues.addAll(validateTurns(r, incomingLanes));
+		}
+		
+		return issues;
+	}
+	
+	private List<Issue> validateTurns(Relation r, Map<IncomingLanes.Key, IncomingLanes> incomingLanes) {
+		final List<Issue> issues = new ArrayList<Issue>();
+		
+		final List<Way> fromWays = Utils.getMemberWays(r, Constants.TURN_ROLE_FROM);
+		final List<Node> viaNodes = Utils.getMemberNodes(r, Constants.TURN_ROLE_VIA);
+		final List<Way> toWays = Utils.getMemberWays(r, Constants.TURN_ROLE_TO);
+		
+		if (fromWays.size() != 1) {
+			issues.add(Issue.newError(r, fromWays, "A turns-relation requires exactly one member-way with role \""
+			    + Constants.TURN_ROLE_FROM + "\"."));
+		}
+		if (viaNodes.size() != 1) {
+			issues.add(Issue.newError(r, viaNodes, "A turns-relation requires exactly one member-node with role \""
+			    + Constants.TURN_ROLE_VIA + "\"."));
+		}
+		if (toWays.size() != 1) {
+			issues.add(Issue.newError(r, toWays, "A turns-relation requires exactly one member-way with role \""
+			    + Constants.TURN_ROLE_TO + "\"."));
+		}
+		
+		if (!issues.isEmpty()) {
+			return issues;
+		}
+		
+		final Way from = fromWays.get(0);
+		final Node via = viaNodes.get(0);
+		final Way to = toWays.get(0);
+		
+		if (!from.isFirstLastNode(via)) {
+			issues.add(Issue.newError(r, from, "The from-way does not start or end at the via-node."));
+		} else if (from.firstNode().equals(from.lastNode())) {
+			issues.add(Issue.newError(r, from, "The from-way both starts as well as ends at the via-node."));
+		}
+		if (!to.isFirstLastNode(via)) {
+			issues.add(Issue.newError(r, to, "The to-way does not start or end at the via-node."));
+		} else if (to.firstNode().equals(to.lastNode())) {
+			issues.add(Issue.newError(r, to, "The to-way both starts as well as ends at the via-node."));
+		}
+		
+		if (!issues.isEmpty()) {
+			return issues;
+		}
+		
+		final IncomingLanes lanes = get(incomingLanes, via, from);
+		
+		for (int l : splitInts(r, Constants.TURN_KEY_LANES, issues)) {
+			if (!lanes.existsRegular(l)) {
+				issues.add(Issue.newError(r, tr("Relation references non-existent (regular) lane {0}", l)));
+			}
+		}
+		
+		for (int l : splitInts(r, Constants.TURN_KEY_EXTRA_LANES, issues)) {
+			if (!lanes.existsExtra(l)) {
+				issues.add(Issue.newError(r, tr("Relation references non-existent extra lane {0}", l)));
+			}
+		}
+		
+		return issues;
+	}
+	
+	private List<Integer> splitInts(Relation r, String key, List<Issue> issues) {
+		final String ints = r.get(key);
+		
+		if (ints == null) {
+			return Collections.emptyList();
+		}
+		
+		final List<Integer> result = new ArrayList<Integer>();
+		
+		for (String s : Constants.SPLIT_PATTERN.split(ints)) {
+			try {
+				int i = Integer.parseInt(s.trim());
+				result.add(Integer.valueOf(i));
+			} catch (NumberFormatException e) {
+				issues.add(Issue.newError(r, tr("Integer list \"{0}\" contains unexpected values.", key)));
+			}
+		}
+		
+		return result;
+	}
+	
+	private IncomingLanes get(Map<IncomingLanes.Key, IncomingLanes> incomingLanes, Node via, Way from) {
+		final IncomingLanes.Key key = new IncomingLanes.Key(via, from);
+		final IncomingLanes lanes = incomingLanes.get(key);
+		
+		if (lanes == null) {
+			final IncomingLanes newLanes = new IncomingLanes(key, 0, Lane.getRegularCount(from, via), 0);
+			incomingLanes.put(key, newLanes);
+			return newLanes;
+		} else {
+			return lanes;
+		}
+	}
+}
