Index: applications/editors/josm/plugins/wms-turbo-challenge2/LICENSE
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/LICENSE	(revision 19990)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/LICENSE	(revision 19990)
@@ -0,0 +1,340 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+	51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
Index: applications/editors/josm/plugins/wms-turbo-challenge2/Makefile
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/Makefile	(revision 19990)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/Makefile	(revision 19990)
@@ -0,0 +1,7 @@
+all: dist install
+dist:
+	ant dist
+clean:
+	ant clean
+install:
+	mv ../../dist/wms-turbo-challenge2.jar ~/.josm/plugins/
Index: applications/editors/josm/plugins/wms-turbo-challenge2/README
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/README	(revision 19990)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/README	(revision 19990)
@@ -0,0 +1,12 @@
+README
+======
+
+This is the Ultimate WMS Turbo Challenge II game, it is used for
+recording GPX traces and/or mapping roads (TODO) based on a WMS layer
+such as Yahoo! aerial imagery.  The WMS source needs to be detailed
+enough for roads to be visible.
+
+If you never recorded GPS traces in a car, now you have a chance to
+do so in an environmentally friendly way.  Note that mapping railways
+or waterways is not supported due to shortage of 4-colour '90s style
+train and motorboat bitmaps.
Index: applications/editors/josm/plugins/wms-turbo-challenge2/build.xml
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/build.xml	(revision 19990)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/build.xml	(revision 19990)
@@ -0,0 +1,117 @@
+<!--
+** To build it run
+**
+**    > ant  dist
+**
+** To install the generated plugin locally (in you default plugin directory) run
+**
+**    > ant  install
+-->
+<project name="wms-turbo-challenge2" default="dist" basedir=".">
+    <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="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"/>
+    <property name="wmsplugin"              location="${plugin.dist.dir}/wmsplugin.jar:${user.home}/.josm/plugins/wmsplugin.jar"/>
+
+    <!--
+    **********************************************************
+    ** init - initializes the build
+    **********************************************************
+    -->
+    <target name="init">
+        <mkdir dir="${plugin.build.dir}"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** compile - compiles the source tree
+    **********************************************************
+    -->
+    <target name="compile" depends="init">
+        <echo message="compiling sources for  ${plugin.jar} ... "/>
+        <javac srcdir="src" classpath="${josm}:${wmsplugin}" debug="true" destdir="${plugin.build.dir}">
+            <compilerarg value="-Xlint:deprecation"/>
+            <compilerarg value="-Xlint:unchecked"/>
+        </javac>
+    </target>
+
+    <!--
+    **********************************************************
+    ** dist - creates the plugin jar
+    **********************************************************
+    -->
+    <target name="dist" depends="compile,revision">
+        <echo message="creating ${plugin.jar.name} ... "/>
+        <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}">
+            <manifest>
+                <attribute name="Author" value="Andrzej Zaborowski"/>
+                <attribute name="Plugin-Class" value="wmsturbochallenge.WMSRacer"/>
+                <attribute name="Plugin-Date" value="${version.entry.commit.date}"/>
+                <attribute name="Plugin-Description" value="Drive a race car from point A to point B over aerial imagery, leave cacti behind."/>
+                <attribute name="Plugin-Link" value="http://wiki.openstreetmap.org/wiki/JOSM/Plugins/WMS_Racer"/>
+                <attribute name="Plugin-Mainversion" value="0.1"/>
+                <attribute name="Plugin-Version" value="${version.entry.commit.revision}"/>
+            </manifest>
+        </jar>
+    </target>
+
+    <!--
+    **********************************************************
+    ** revision - extracts the current revision number for the
+    **    file build.number and stores it in the XML property
+    **    version.*
+    **********************************************************
+    -->
+    <target name="revision">
+
+        <exec append="false" output="REVISION" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="info"/>
+            <arg value="--xml"/>
+            <arg value="."/>
+        </exec>
+        <xmlproperty file="REVISION" prefix="version" keepRoot="false" collapseAttributes="true"/>
+        <delete file="REVISION"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** clean - clean up the build environment
+    **********************************************************
+    -->
+    <target name="clean">
+        <delete dir="${plugin.build.dir}"/>
+        <delete file="${plugin.jar}"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** install - install the plugin in your local JOSM installation
+    **********************************************************
+    -->
+    <target name="install" depends="dist">
+        <property environment="env"/>
+        <condition property="josm.plugins.dir" value="${env.APPDATA}/JOSM/plugins" else="${user.home}/.josm/plugins">
+            <and>
+                <os family="windows"/>
+            </and>
+        </condition>
+        <copy file="${plugin.jar}" todir="${josm.plugins.dir}"/>
+    </target>
+</project>
Index: applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/EngineSound.java
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/EngineSound.java	(revision 19990)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/EngineSound.java	(revision 19990)
@@ -0,0 +1,165 @@
+/*
+ * GPLv2 or 3, Copyright (c) 2010  Andrzej Zaborowski
+ *
+ * This class simulates a car engine.  What does a car engine do?  It
+ * makes a pc-speaker-like buzz.  The PC Speaker could only emit
+ * a (nearly) square wave and we simulate it here for maximum realism.
+ */
+package wmsturbochallenge;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.SourceDataLine;
+import javax.sound.sampled.AudioFormat;
+
+class engine {
+	public engine() {
+		rpm = 0.0;
+	}
+
+	public void start() {
+		rpm = 0.3;
+		speed = 0.0;
+		n = 0;
+
+		if (output != null)
+			stop();
+
+		AudioFormat output_format =
+			new AudioFormat(S_RATE, 16, 1, true, true);
+		DataLine.Info info =
+			new DataLine.Info(SourceDataLine.class, output_format);
+
+		/* Get the data line, open it and initialise the device */
+		try {
+			output = (SourceDataLine) AudioSystem.getLine(info);
+			output.open(output_format);
+			output.start();
+			frames_written = 0;
+			reschedule(0);
+		} catch (Exception e) {
+			output = null;
+			System.out.println("Audio not available: " +
+					e.getClass().getSimpleName());
+		}
+	}
+
+	public void stop() {
+		rpm = 0.0;
+		n = 0;
+
+		if (output == null)
+			return;
+
+		tick.cancel();
+		tick.purge();
+
+		output.stop();
+		output.flush();
+		output.close();
+		output = null;
+	}
+
+	public void set_speed(double speed) {
+		/* This engine is equipped with an automatic gear box that
+		 * switches gears when the RPM becomes too high or too low.  */
+		double new_speed = Math.abs(speed);
+		double accel = new_speed - this.speed;
+		this.speed = new_speed;
+
+		if (accel > 0.05)
+			accel = 0.05;
+		else if (accel < -0.05)
+			accel = -0.05;
+		rpm += accel;
+
+		if (accel > 0.0 && rpm > 1.0 + n * 0.2 && speed > 0.0) {
+			rpm = 0.3 + n * 0.2;
+			n ++;
+		} else if (accel < 0.0 && rpm < 0.3) {
+			if (n > 0) {
+				rpm = 0.7 + n * 0.1;
+				n --;
+			} else
+				rpm = 0.2;
+		}
+		if (speed < 2.0)
+			n = 0;
+	}
+
+	public boolean is_on() {
+		return output != null;
+	}
+
+	protected double speed;
+	protected double rpm;
+	protected int n;
+
+	protected SourceDataLine output = null;
+	protected long frames_written;
+	protected Timer tick = new Timer();
+
+	/* Audio parameters.  */
+	protected static final int S_RATE = 44100;
+	protected static final int MIN_BUFFER = 4096;
+	protected static final double volume = 0.3;
+
+	protected class audio_task extends TimerTask {
+		public void run() {
+			if (output == null)
+				return;
+
+			/* If more than a two buffers left to play,
+			 * reschedule and try to wake up closer to the
+			 * end of already written data.  */
+			long frames_current = output.getLongFramePosition();
+			if (frames_current < frames_written - MIN_BUFFER * 2) {
+				reschedule(frames_current);
+				return;
+			}
+
+			/* Build a new buffer */
+			/* double freq = 20 * Math.pow(1.3, rpm * 5.0); */
+			double freq = (rpm - 0.1) * 160.0;
+			int wavelen = (int) (S_RATE / freq);
+			int bufferlen = MIN_BUFFER - (MIN_BUFFER % wavelen) +
+				wavelen;
+			int value = (int) (0x7fff * volume);
+
+			bufferlen *= 2;
+			byte[] buffer = new byte[bufferlen];
+			for (int b = 0; b < bufferlen; ) {
+				int j;
+				for (j = wavelen / 2; j > 0; j --) {
+					buffer[b ++] = (byte) (value >> 8);
+					buffer[b ++] = (byte) (value & 0xff);
+				}
+				value = 0x10000 - value;
+				for (j = wavelen - wavelen / 2; j > 0; j --) {
+					buffer[b ++] = (byte) (value >> 8);
+					buffer[b ++] = (byte) (value & 0xff);
+				}
+				value = 0x10000 - value;
+			}
+
+			frames_written +=
+				output.write(buffer, 0, bufferlen) / 2;
+
+			reschedule(frames_current);
+		}
+	}
+
+	protected void reschedule(long frames) {
+		/* Send a new buffer as close to the end of the
+		 * currently playing buffer as possible (aim at
+		 * about half into the last frame).  */
+		long delay = (frames_written - frames - MIN_BUFFER / 2) *
+			1000 / S_RATE;
+		if (delay < 0)
+			delay = 0;
+		tick.schedule(new audio_task(), delay);
+	}
+}
Index: applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/FakeMapView.java
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/FakeMapView.java	(revision 19990)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/FakeMapView.java	(revision 19990)
@@ -0,0 +1,130 @@
+/*
+ * GPLv2 or 3, Copyright (c) 2010  Andrzej Zaborowski
+ *
+ * Implements a fake MapView that we can pass to WMSLayer's .paint,
+ * this will give us two things:
+ *  # We'll be able to tell WMSLayer.paint() what area we want it
+ *    to download (it ignores the "bounds" parameter) and override
+ *    isVisible and friends as needed, and
+ *  # We'll receive notifications from Grabber when we need to
+ *    repaint (and call WMSLayer's .paint again) because the
+ *    Grabber downloaded some or all of the tiles that we asked
+ *    WMSLayer for and WMSLayer created the Grabber passing it
+ *    our MapView.  Otherwise we wouldn't be able to tell when
+ *    this happened and could only guess.
+ */
+package wmsturbochallenge;
+
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+
+import java.awt.Point;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+
+class fake_map_view extends MapView {
+	public ProjectionBounds view_bounds;
+	public MapView parent;
+
+	public Graphics2D graphics;
+	public BufferedImage ground_image;
+	public int ground_width = -1;
+	public int ground_height = -1;
+	public double scale;
+	public double max_east_west;
+
+	public fake_map_view(MapView parent, double scale) {
+		this.parent = parent;
+		this.scale = scale;
+
+		ProjectionBounds parent_bounds = parent.getProjectionBounds();
+		max_east_west =
+			parent_bounds.max.east() - parent_bounds.min.east();
+	}
+
+	public void setProjectionBounds(ProjectionBounds bounds) {
+		view_bounds = bounds;
+
+		if (bounds.max.east() - bounds.min.east() > max_east_west) {
+			max_east_west = bounds.max.east() - bounds.min.east();
+
+			/* We need to set the parent MapView's bounds (i.e.
+			 * zoom level) to the same as ours max possible
+			 * bounds to avoid WMSLayer thinking we're zoomed
+			 * out more than we are or it'll pop up an annoying
+			 * "requested area is too large" popup.
+			 */
+			EastNorth parent_center = parent.getCenter();
+			parent.zoomTo(new ProjectionBounds(
+					new EastNorth(
+						parent_center.east() -
+						max_east_west / 2,
+						parent_center.north()),
+					new EastNorth(
+						parent_center.east() +
+						max_east_west / 2,
+						parent_center.north())));
+
+			/* Request again because NavigatableContent adds
+			 * a border just to be sure.
+			 */
+			ProjectionBounds new_bounds =
+				parent.getProjectionBounds();
+			max_east_west =
+				new_bounds.max.east() - new_bounds.min.east();
+		}
+
+		Point vmin = getPoint(bounds.min);
+		Point vmax = getPoint(bounds.max);
+		int w = vmax.x + 1;
+		int h = vmin.y + 1;
+
+		if (w <= ground_width && h <= ground_height) {
+			graphics.setClip(0, 0, w, h);
+			return;
+		}
+
+		if (w > ground_width)
+			ground_width = w;
+		if (h > ground_height)
+			ground_height = h;
+
+		ground_image = new BufferedImage(ground_width,
+				ground_height,
+				BufferedImage.TYPE_INT_RGB);
+		graphics = ground_image.createGraphics();
+		graphics.setClip(0, 0, w, h);
+	}
+
+	public ProjectionBounds getProjectionBounds() {
+		return view_bounds;
+	}
+
+	public Point getPoint(EastNorth p) {
+		double x = p.east() - view_bounds.min.east();
+		double y = view_bounds.max.north() - p.north();
+		x /= this.scale;
+		y /= this.scale;
+
+		return new Point((int) x, (int) y);
+	}
+
+	public EastNorth getEastNorth(int x, int y) {
+		return new EastNorth(
+			view_bounds.min.east() + x * this.scale,
+			view_bounds.min.north() - y * this.scale);
+	}
+
+	public boolean isVisible(int x, int y) {
+		return true;
+	}
+
+	public Graphics getGraphics() {
+		return graphics;
+	}
+
+	public void repaint() {
+	}
+}
Index: applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/GameWindow.java
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/GameWindow.java	(revision 19990)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/GameWindow.java	(revision 19990)
@@ -0,0 +1,701 @@
+/*
+ * GPLv2 or 3, Copyright (c) 2010  Andrzej Zaborowski
+ *
+ * This implements the game logic.
+ */
+package wmsturbochallenge;
+
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.Main;
+
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.data.gpx.GpxTrack;
+import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
+import org.openstreetmap.josm.data.gpx.WayPoint;
+
+import java.util.Collection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyAdapter;
+import java.awt.Toolkit;
+import java.awt.Graphics;
+import java.awt.Color;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.ImageIcon;
+import javax.swing.Timer;
+
+public class GameWindow extends JFrame implements ActionListener {
+	public GameWindow(Layer ground) {
+		setTitle("The Ultimate WMS Super-speed Turbo Challenge II");
+		setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+		setUndecorated(true);
+		setSize(s.getScreenSize().width, s.getScreenSize().height);
+		setLocationRelativeTo(null);
+		setResizable(false);
+
+		while (s.getScreenSize().width < width * scale ||
+				s.getScreenSize().height < height * scale)
+			scale --;
+		add(panel);
+
+		setVisible(true);
+
+		/* TODO: "Intro" screen perhaps with "Hall of Fame" */
+
+		screen_image = new BufferedImage(width, height,
+				BufferedImage.TYPE_INT_RGB);
+		screen = screen_image.getGraphics();
+
+		this.ground = ground;
+		ground_view = new fake_map_view(Main.map.mapView, 0.0000001);
+
+		/* Retrieve start position */
+		EastNorth start = ground_view.parent.getCenter();
+		lat = start.north();
+		lon = start.east();
+
+		addKeyListener(new TAdapter());
+
+		timer = new Timer(80, this);
+		timer.start();
+
+		car_gps = new gps();
+		car_gps.start();
+
+		car_engine = new engine();
+		car_engine.start();
+
+		for (int i = 0; i < maxsprites; i ++)
+			sprites[i] = new sprite_pos();
+
+		generate_sky();
+	}
+
+	protected engine car_engine;
+
+	protected gps car_gps;
+	protected class gps extends Timer implements ActionListener {
+		public gps() {
+			super(1000, null);
+			addActionListener(this);
+
+			trackSegs = new ArrayList<Collection<WayPoint>>();
+		}
+
+		protected Collection<WayPoint> segment;
+		protected Collection<Collection<WayPoint>> trackSegs;
+
+		public void actionPerformed(ActionEvent e) {
+			/* We should count the satellites here, see if we
+			 * have a fix and add any distortions.  */
+
+			segment.add(new WayPoint(Main.proj.eastNorth2latlon(
+					new EastNorth(lon, lat))));
+		}
+
+		public void start() {
+			super.start();
+
+			/* Start recording */
+			segment = new ArrayList<WayPoint>();
+			trackSegs.add(segment);
+			actionPerformed(null);
+		}
+
+		public void save_trace() {
+			int len = 0;
+			for (Collection<WayPoint> seg : trackSegs)
+				len += seg.size();
+
+			/* Don't save traces shorter than 5s */
+			if (len <= 5)
+				return;
+
+			GpxData data = new GpxData();
+			data.tracks.add(new ImmutableGpxTrack(trackSegs,
+						new HashMap<String, Object>()));
+
+			ground_view.parent.addLayer(
+					new GpxLayer(data, "Car GPS trace"));
+		}
+	}
+
+	/* These are EastNorth, not actual LatLon */
+	protected double lat, lon;
+	/* Camera's altitude above surface (same units as lat/lon above) */
+	protected double ele = 0.000003;
+	/* Cut off at ~75px from bottom of the screen */
+	protected double horizon = 0.63;
+	/* Car's distance from the camera lens */
+	protected double cardist = ele * 3;
+
+	/* Pixels per pixel, the bigger the more oldschool :-)  */
+	protected int scale = 5;
+
+	protected BufferedImage screen_image;
+	protected Graphics screen;
+	protected int width = 320;
+	protected int height = 200;
+	protected int centre = width / 2;
+
+	double maxdist = ele / (horizon - 0.6);
+	double realwidth = maxdist * width / height;
+	double pixelperlat = 1.0 * width / realwidth;
+	double sratio = 0.85;
+	protected int sw = (int) (2 * Math.PI * maxdist * pixelperlat * sratio);
+
+	/* TODO: figure out how to load these dynamically after splash
+	 * screen is shown */
+	protected static final ImageIcon car[] = new ImageIcon[] {
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/car0-l.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/car0.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/car0-r.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/car1-l.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/car1.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/car1-r.png"))),
+	};
+	protected static final ImageIcon bg[] = new ImageIcon[] {
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/bg0.png"))),
+	};
+	protected static final ImageIcon skyline[] = new ImageIcon[] {
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/horizon.png"))),
+	};
+	protected static final ImageIcon cactus[] = new ImageIcon[] {
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/cactus0.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/cactus1.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/cactus2.png"))),
+	};
+	protected static final ImageIcon cloud[] = new ImageIcon[] {
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/cloud0.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/cloud1.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/cloud2.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/cloud3.png"))),
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/cloud4.png"))),
+	};
+	protected static final ImageIcon aircraft[] = new ImageIcon[] {
+	        new ImageIcon(Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/aircraft0.png"))),
+	};
+	protected static final ImageIcon loading = new ImageIcon(
+			Toolkit.getDefaultToolkit().createImage(
+					WMSRacer.class.getResource(
+						"/images/loading.png")));
+	protected static Toolkit s = Toolkit.getDefaultToolkit();
+	protected int current_bg = 0;
+	protected int current_car = 0;
+	protected boolean cacti_on = true;
+	protected List<EastNorth> cacti = new ArrayList<EastNorth>();
+	protected List<EastNorth> todelete = new ArrayList<EastNorth>();
+	protected int splashframe = -1;
+	protected EastNorth splashcactus;
+
+	protected Layer ground;
+	protected double heading = 0.0;
+	protected double wheelangle = 0.0;
+	protected double speed = 0.0;
+	protected boolean key_down[] = new boolean[] {
+		false, false, false, false, };
+
+	protected void move() {
+		/* Left */
+		/* (At high speeds make more gentle turns) */
+		if (key_down[0])
+			wheelangle -= 0.1 / (1.0 + Math.abs(speed));
+		/* Right */
+		if (key_down[1])
+			wheelangle += 0.1 / (1.0 + Math.abs(speed));
+		if (wheelangle > 0.3)
+			wheelangle = 0.3; /* Radians */
+		if (wheelangle < -0.3)
+			wheelangle = -0.3;
+
+		wheelangle *= 0.7;
+
+		/* Up */
+		if (key_down[2])
+			speed += speed >= 0.0 ? 1.0 / (2.0 + speed) : 0.5;
+		/* Down */
+		if (key_down[3]) {
+			if (speed >= 0.5) /* Brake (TODO: sound) */
+				speed -= 0.5;
+			else if (speed >= 0.01) /* Brake (TODO: sound) */
+				speed = 0.0;
+			else /* Reverse */
+				speed -= 0.5 / (4.0 - speed);
+		}
+
+		speed *= 0.97;
+		car_engine.set_speed(speed);
+
+		if (speed > -0.1 && speed < 0.1)
+			speed = 0;
+
+		heading += wheelangle * speed;
+
+		boolean chop = false;
+		double newlat = lat + Math.cos(heading) * speed * ele * 0.2;
+		double newlon = lon + Math.sin(heading) * speed * ele * 0.2;
+		for (EastNorth pos : cacti) {
+			double alat = Math.abs(pos.north() - newlat);
+			double alon = Math.abs(pos.east() - newlon);
+			if (alat + alon < ele * 1.0) {
+				if (Math.abs(speed) < 2.0) {
+					if (speed > 0.0)
+						speed = -0.5;
+					else
+						speed = 0.3;
+					newlat = lat;
+					newlon = lon;
+					break;
+				}
+
+				chop = true;
+				splashframe = 0;
+				splashcactus = pos;
+				todelete.add(pos);
+			}
+		}
+
+		lat = newlat;
+		lon = newlon;
+
+		/* Seed a new cactus if we're moving.
+		 * TODO: hook into data layers and avoid putting the cactus on
+		 * the road!
+		 */
+		if (cacti_on && Math.random() * 30.0 < speed) {
+			double left_x = maxdist * (width - centre) / height;
+			double right_x = maxdist * (0 - centre) / height;
+			double x = left_x + Math.random() * (right_x - left_x);
+			double clat = lat + (maxdist - cardist) *
+				Math.cos(heading) - x * Math.sin(heading);
+			double clon = lon + (maxdist - cardist) *
+				Math.sin(heading) + x * Math.cos(heading);
+
+			cacti.add(new EastNorth(clon, clat));
+			chop = true;
+		}
+
+		/* Chop down any cactus far enough that it can't
+		 * be seen.  ``If a cactus falls in a forest and
+		 * there is nobody around did it make a sound?''
+		 */
+		if (chop) {
+			for (EastNorth pos : cacti) {
+				double alat = Math.abs(pos.north() - lat);
+				double alon = Math.abs(pos.east() - lon);
+				if (alat + alon > 2 * maxdist)
+					todelete.add(pos);
+			}
+			cacti.removeAll(todelete);
+			todelete = new ArrayList<EastNorth>();
+		}
+	}
+
+	int frame;
+	boolean downloading = false;
+	protected void screen_repaint() {
+		/* Draw background first */
+		sky_paint();
+
+		/* On top of it project the floor */
+		ground_paint();
+
+		/* Messages */
+		frame ++;
+		if ((frame & 8) == 0 && downloading)
+			screen.drawImage(loading.getImage(), centre -
+					loading.getIconWidth() / 2, 50, this);
+
+		/* Sprites */
+		sprites_paint();
+	}
+
+	static double max3(double x[]) {
+		return x[0] > x[1] ? x[2] > x[0] ? x[2] : x[0] :
+			(x[2] > x[1] ? x[2] : x[1]);
+	}
+	static double min3(double x[]) {
+		return x[0] < x[1] ? x[2] < x[0] ? x[2] : x[0] :
+			(x[2] < x[1] ? x[2] : x[1]);
+	}
+
+	protected void ground_paint() {
+		double sin = Math.sin(heading);
+		double cos = Math.cos(heading);
+
+		/* First calculate the bounding box for the visible area.
+		 * The area will be (nearly) a triangle, so calculate the
+		 * EastNorth for the three corners and make a bounding box.
+		 */
+		double left_x = maxdist * (width - centre) / height;
+		double right_x = maxdist * (0 - centre) / height;
+		double e_lat[] = new double[] {
+			lat + (maxdist - cardist) * cos - left_x * sin,
+			lat + (maxdist - cardist) * cos - right_x * sin,
+			lat - cardist * cos, };
+		double e_lon[] = new double[] {
+			lon + (maxdist - cardist) * sin + left_x * cos,
+			lon + (maxdist - cardist) * sin + right_x * cos,
+			lon - cardist * sin, };
+		ground_view.setProjectionBounds(new ProjectionBounds(
+				new EastNorth(min3(e_lon), min3(e_lat)),
+				new EastNorth(max3(e_lon), max3(e_lat))));
+
+		/* If the layer is a WMS layer, check if any tiles are
+		 * missing */
+		if (ground instanceof wmsplugin.WMSLayer) {
+			wmsplugin.WMSLayer wms = (wmsplugin.WMSLayer) ground;
+			downloading = wms.hasAutoDownload() && (
+					null == wms.findImage(new EastNorth(
+							e_lon[0], e_lat[0])) ||
+					null == wms.findImage(new EastNorth(
+							e_lon[0], e_lat[0])) ||
+					null == wms.findImage(new EastNorth(
+							e_lon[0], e_lat[0])));
+		}
+
+		/* Request the image from ground layer */
+		ground.paint(ground_view.graphics, ground_view, null);
+
+		for (int y = (int) (height * horizon + 0.1); y < height; y ++) {
+			/* Assume a 60 deg vertical Field of View when
+			 * calculating the distance at given pixel.  */
+			double dist = ele / (1.0 * y / height - 0.6);
+			double lat_off = lat + (dist - cardist) * cos;
+			double lon_off = lon + (dist - cardist) * sin;
+
+			for (int x = 0; x < width; x ++) {
+				double p_x = dist * (x - centre) / height;
+
+				EastNorth en = new EastNorth(
+						lon_off + p_x * cos,
+						lat_off - p_x * sin);
+
+				Point pt = ground_view.getPoint(en);
+
+				int rgb = ground_view.ground_image.getRGB(
+						pt.x, pt.y);
+				screen_image.setRGB(x, y, rgb);
+			}
+		}
+	}
+
+	protected BufferedImage sky_image;
+	protected Graphics sky;
+	public void generate_sky() {
+		sky_image = new BufferedImage(sw, 70,
+				BufferedImage.TYPE_INT_ARGB);
+		sky = sky_image.getGraphics();
+
+		int n = (int) (Math.random() * sw * 0.03);
+		for (int i = 0; i < n; i ++) {
+			int t = (int) (Math.random() * 5.0);
+			int x = (int) (Math.random() *
+					(sw - cloud[t].getIconWidth()));
+			int y = (int) ((1 - Math.random() * Math.random()) *
+					(70 - cloud[t].getIconHeight()));
+			sky.drawImage(cloud[t].getImage(), x, y, this);
+		}
+
+		if (Math.random() < 0.5) {
+			int t = 0;
+			int x = (int) (300 + Math.random() * (sw - 500 -
+						aircraft[t].getIconWidth()));
+			sky.drawImage(aircraft[t].getImage(), x, 0, this);
+		}
+	}
+
+	public void sky_paint() {
+		/* for x -> 0, lim sin(x) / x = 1 */
+		int hx = (int) (-heading * maxdist * pixelperlat);
+		int hw = skyline[current_bg].getIconWidth();
+		hx = ((hx % hw) - hw) % hw;
+
+		int sx = (int) (-heading * maxdist * pixelperlat * sratio);
+		sx = ((sx % sw) - sw) % sw;
+
+		screen.drawImage(bg[current_bg].getImage(), 0, 0, this);
+		screen.drawImage(sky_image, sx, 50, this);
+		if (sw + sx < width)
+			screen.drawImage(sky_image, sx + sw, 50, this);
+		screen.drawImage(skyline[current_bg].getImage(), hx, 66, this);
+		if (hw + hx < width)
+			screen.drawImage(skyline[current_bg].getImage(),
+					hx + hw, 66, this);
+	}
+
+	protected class sprite_pos implements Comparable {
+		double dist;
+
+		int x, y, sx, sy;
+		Image sprite;
+
+		public sprite_pos() {
+		}
+
+		public int compareTo(Object x) {
+			sprite_pos other = (sprite_pos) x;
+			return (int) ((other.dist - this.dist) * 1000000.0);
+		}
+	}
+
+	/* sizes decides how many zoom levels the sprites have.  We
+	 * could do just normal scalling according to distance but
+	 * that's not what old games did, they had prescaled sprites
+	 * for the different distances and you could see the feature
+	 * grow discretely as you approached it.  */
+	protected final static int sizes = 8;
+
+	protected final static int maxsprites = 32;
+	protected sprite_pos sprites[] = new sprite_pos[maxsprites];
+
+	protected void sprites_paint() {
+		/* The vehicle */
+		int orientation = (wheelangle > -0.02 ? wheelangle < 0.02 ?
+				1 : 2 : 0) + current_car * 3;
+		sprites[0].sprite = car[orientation].getImage();
+		sprites[0].dist = cardist;
+		sprites[0].sx = car[orientation].getIconWidth();
+		sprites[0].x = centre - sprites[0].sx / 2;
+		sprites[0].sy = car[orientation].getIconHeight();
+		sprites[0].y = height - sprites[0].sy - 10; /* TODO */
+
+		/* The cacti */
+		double sin = Math.sin(-heading);
+		double cos = Math.cos(-heading);
+		int i = 1;
+
+		for (EastNorth ll : cacti) {
+			double clat = ll.north() - lat;
+			double clon = ll.east() - lon;
+			double dist = (clat * cos - clon * sin) + cardist;
+			double p_x = clat * sin + clon * cos;
+
+			if (dist * 8 <= cardist || dist > maxdist)
+				continue;
+
+			int x = (int) (p_x * height / dist + centre);
+			int y = (int) ((ele / dist + 0.6) * height);
+
+			if (i >= maxsprites)
+				break;
+			if (x < -10 || x > width + 10)
+				continue;
+
+			int type = (((int) (ll.north() * 10000000.0) & 31) % 3);
+			int sx = cactus[type].getIconWidth();
+			int sy = cactus[type].getIconHeight();
+
+			sprite_pos pos = sprites[i ++];
+			pos.dist = dist;
+			pos.sprite = cactus[type].getImage();
+			pos.sx = (int) (sx * cardist * 0.7 / dist);
+			pos.sy = (int) (sy * cardist * 0.7 / dist);
+			pos.x = x - pos.sx / 2;
+			pos.y = y - pos.sy;
+		}
+
+		Arrays.sort(sprites, 0, i);
+		for (sprite_pos sprite : sprites)
+			if (i --> 0)
+				screen.drawImage(sprite.sprite,
+						sprite.x, sprite.y,
+						sprite.sx, sprite.sy, this);
+			else
+				break;
+
+		if (splashframe >= 0) {
+			splashframe ++;
+			if (splashframe >= 8)
+				splashframe = -1;
+
+			int type = (((int) (splashcactus.north() *
+							10000000.0) & 31) % 3);
+			int sx = cactus[type].getIconWidth();
+			int sy = cactus[type].getIconHeight();
+			Image image = cactus[type].getImage();
+
+			for (i = 0; i < 50; i ++) {
+				int x = (int) (Math.random() * sx);
+				int y = (int) (Math.random() * sy);
+				int w = (int) (Math.random() * 20);
+				int h = (int) (Math.random() * 20);
+				int nx = centre + splashframe * (x - sx / 2);
+				int ny = height - splashframe * (sy - y);
+				int nw = w + splashframe;
+				int nh = h + splashframe;
+
+				screen.drawImage(image,
+						nx, ny, nx + nw, ny + nh,
+						x, y, x + w, y + h, this);
+			}
+		}
+	}
+
+	public boolean no_super_repaint = false;
+	protected class GamePanel extends JPanel {
+		public GamePanel() {
+			setBackground(Color.BLACK);
+			setDoubleBuffered(true);
+		}
+
+		public void paint(Graphics g) {
+			int w = (int) getSize().getWidth();
+			int h = (int) getSize().getHeight();
+
+			if (no_super_repaint)
+				no_super_repaint = false;
+			else
+				super.paint(g);
+
+			g.drawImage(screen_image, (w - width * scale) / 2,
+					(h - height * scale) / 2,
+					width * scale, height * scale, this);
+
+			Toolkit.getDefaultToolkit().sync();
+		}
+	}
+	JPanel panel = new GamePanel();
+
+	protected void quit() {
+		timer.stop();
+
+		car_engine.stop();
+
+		car_gps.stop();
+		car_gps.save_trace();
+
+		setVisible(false);
+		panel = null;
+		screen_image = null;
+		screen = null;
+		dispose();
+	}
+
+	/*
+	 * Supposedly a thread drawing frames and sleeping in a loop is
+	 * better than for animating than swing Timers.  For the moment
+	 * I'll use a timer because I don't want to deal with all the
+	 * potential threading issues.
+	 */
+	protected Timer timer;
+	public void actionPerformed(ActionEvent e) {
+		move();
+		screen_repaint();
+
+		no_super_repaint = true;
+		panel.repaint();
+	}
+
+	protected class TAdapter extends KeyAdapter {
+		public void keyPressed(KeyEvent e) {
+			int key = e.getKeyCode();
+
+			if (key == KeyEvent.VK_LEFT && !key_down[0]) {
+				wheelangle -= 0.02;
+				key_down[0] = true;
+			}
+
+			if (key == KeyEvent.VK_RIGHT && !key_down[1]) {
+				wheelangle += 0.02;
+				key_down[1] = true;
+			}
+
+			if (key == KeyEvent.VK_UP)
+				key_down[2] = true;
+
+			if (key == KeyEvent.VK_DOWN)
+				key_down[3] = true;
+
+			if (key == KeyEvent.VK_ESCAPE)
+				quit();
+
+			/* Toggle sound */
+			if (key == KeyEvent.VK_S) {
+				if (car_engine.is_on())
+					car_engine.stop();
+				else
+					car_engine.start();
+			}
+
+			/* Toggle cacti */
+			if (key == KeyEvent.VK_C) {
+				cacti_on = !cacti_on;
+				if (!cacti_on)
+					cacti = new ArrayList<EastNorth>();
+			}
+
+			/* Switch vehicle */
+			if (key == KeyEvent.VK_V)
+				if (current_car ++>= 1)
+					current_car = 0;
+		}
+
+		public void keyReleased(KeyEvent e) {
+			int key = e.getKeyCode();
+
+			if (key == KeyEvent.VK_LEFT)
+				key_down[0] = false;
+
+			if (key == KeyEvent.VK_RIGHT)
+				key_down[1] = false;
+
+			if (key == KeyEvent.VK_UP)
+				key_down[2] = false;
+
+			if (key == KeyEvent.VK_DOWN)
+				key_down[3] = false;
+		}
+	}
+	protected fake_map_view ground_view;
+}
Index: applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/WMSRacer.java
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/WMSRacer.java	(revision 19990)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/WMSRacer.java	(revision 19990)
@@ -0,0 +1,114 @@
+/*
+ * GPLv2 or 3, Copyright (c) 2010  Andrzej Zaborowski
+ *
+ * This is the main class for the game plugin.
+ */
+package wmsturbochallenge;
+
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.Main;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+
+import java.awt.event.ActionEvent;
+
+public class WMSRacer implements LayerChangeListener {
+	public WMSRacer() {
+		driveAction.updateEnabledState();
+
+		JMenu toolsMenu = Main.main.menu.toolsMenu;
+		toolsMenu.addSeparator();
+		toolsMenu.add(new JMenuItem(driveAction));
+	}
+
+	/* Rather than add an action or main menu entry we should add
+	 * an entry in the new layer's context menus in layerAdded
+	 * but there doesn't seem to be any way to do that :( */
+	protected class DriveAction extends JosmAction {
+		public MapFrame frame = null;
+		public Layer currentLayer = null;
+		protected Layer groundLayer = null;
+
+		public DriveAction() {
+			super("Go driving", "wmsracer",
+					"Drive a race car on this layer",
+					null, true);
+			setEnabled(false);
+		}
+
+		public void actionPerformed(ActionEvent ev) {
+			if (groundLayer == null ||
+					!groundLayer.isBackgroundLayer())
+				return;
+
+			new GameWindow(groundLayer);
+		}
+
+		public void updateEnabledState() {
+			if (frame == null) {
+				groundLayer = null;
+				setEnabled(false);
+				return;
+			}
+
+			if (currentLayer != null &&
+					currentLayer.isBackgroundLayer()) {
+				groundLayer = currentLayer;
+				setEnabled(true);
+				return;
+			}
+
+			/* TODO: should only iterate through visible layers?
+			 * or only wms layers? or perhaps we should allow
+			 * driving on data/gpx layers too, or the full layer
+			 * stack (by calling mapView.paint() instead of
+			 * layer.paint()?  Nah.
+			 * (Note that for GPX or Data layers we could do
+			 * some clever rendering directly on our perspectivic
+			 * pseudo-3d surface by defining a strange projection
+			 * like that or rendering in "stripes" at different
+			 * horizontal scanlines (lines equidistant from
+			 * camera eye)) */
+			for (Layer l : frame.mapView.getAllLayers())
+				if (l.isBackgroundLayer()) {
+					groundLayer = l;
+					setEnabled(true);
+					return;
+				}
+
+			groundLayer = null;
+			setEnabled(false);
+		}
+	}
+
+	protected DriveAction driveAction = new DriveAction();
+
+	public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+		if (oldFrame != null)
+			oldFrame.mapView.removeLayerChangeListener(this);
+
+		driveAction.frame = newFrame;
+		driveAction.updateEnabledState();
+
+		if (newFrame != null)
+			newFrame.mapView.addLayerChangeListener(this);
+	}
+
+	/* LayerChangeListener methods */
+	public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+		driveAction.currentLayer = newLayer;
+		driveAction.updateEnabledState();
+	}
+
+	public void layerAdded(Layer newLayer) {
+		driveAction.updateEnabledState();
+	}
+
+	public void layerRemoved(Layer oldLayer) {
+		driveAction.updateEnabledState();
+	}
+}
