Index: applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/EngineSound.java
===================================================================
--- applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/EngineSound.java	(revision 21761)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/EngineSound.java	(revision 23190)
@@ -17,149 +17,149 @@
 
 class engine {
-	public engine() {
-		rpm = 0.0;
-	}
+    public engine() {
+        rpm = 0.0;
+    }
 
-	public void start() {
-		rpm = 0.3;
-		speed = 0.0;
-		n = 0;
+    public void start() {
+        rpm = 0.3;
+        speed = 0.0;
+        n = 0;
 
-		if (output != null)
-			stop();
+        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);
+        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());
-		}
-	}
+        /* 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;
+    public void stop() {
+        rpm = 0.0;
+        n = 0;
 
-		if (output == null)
-			return;
+        if (output == null)
+            return;
 
-		tick.cancel();
-		tick.purge();
+        tick.cancel();
+        tick.purge();
 
-		output.stop();
-		output.flush();
-		output.close();
-		output = null;
-	}
+        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;
+    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.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;
-	}
+        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;
-	}
+    public boolean is_on() {
+        return output != null;
+    }
 
-	protected double speed;
-	protected double rpm;
-	protected int n;
+    protected double speed;
+    protected double rpm;
+    protected int n;
 
-	protected SourceDataLine output = null;
-	protected long frames_written;
-	protected Timer tick = new Timer();
+    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;
+    /* 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;
+    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;
-			}
+            /* 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);
+            /* 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;
-			}
+            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;
+            frames_written +=
+                output.write(buffer, 0, bufferlen) / 2;
 
-			reschedule(frames_current);
-		}
-	}
+            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);
-	}
+    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 21761)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/FakeMapView.java	(revision 23190)
@@ -26,106 +26,106 @@
 
 class fake_map_view extends MapView {
-	public ProjectionBounds view_bounds;
-	public MapView parent;
+    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 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) {
-		super(null); //TODO MapView constructor contains registering listeners and other code, that probably shouldn't be called in fake map view
-		this.parent = parent;
-		this.scale = scale;
+    public fake_map_view(MapView parent, double scale) {
+        super(null); //TODO MapView constructor contains registering listeners and other code, that probably shouldn't be called in fake map view
+        this.parent = parent;
+        this.scale = scale;
 
-		ProjectionBounds parent_bounds = parent.getProjectionBounds();
-		max_east_west =
-			parent_bounds.max.east() - parent_bounds.min.east();
-	}
+        ProjectionBounds parent_bounds = parent.getProjectionBounds();
+        max_east_west =
+            parent_bounds.max.east() - parent_bounds.min.east();
+    }
 
-	public void setProjectionBounds(ProjectionBounds bounds) {
-		view_bounds = bounds;
+    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();
+        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())));
+            /* 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();
-		}
+            /* 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;
+        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 && h <= ground_height) {
+            graphics.setClip(0, 0, w, h);
+            return;
+        }
 
-		if (w > ground_width)
-			ground_width = w;
-		if (h > ground_height)
-			ground_height = h;
+        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);
-	}
+        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 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;
+    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);
-	}
+        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 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 boolean isVisible(int x, int y) {
+        return true;
+    }
 
-	public Graphics getGraphics() {
-		return graphics;
-	}
+    public Graphics getGraphics() {
+        return graphics;
+    }
 
-	public void repaint() {
-	}
+    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 21761)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/GameWindow.java	(revision 23190)
@@ -42,660 +42,660 @@
 
 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;
+    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 21761)
+++ applications/editors/josm/plugins/wms-turbo-challenge2/src/wmsturbochallenge/WMSRacer.java	(revision 23190)
@@ -20,98 +20,98 @@
 
 public class WMSRacer extends Plugin implements LayerChangeListener {
-	public WMSRacer(PluginInformation info) {
-		super(info);
-		driveAction.updateEnabledState();
+    public WMSRacer(PluginInformation info) {
+        super(info);
+        driveAction.updateEnabledState();
 
-		JMenu toolsMenu = Main.main.menu.toolsMenu;
-		toolsMenu.addSeparator();
-		toolsMenu.add(new JMenuItem(driveAction));
-	}
+        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;
+    /* 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 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;
+        public void actionPerformed(ActionEvent ev) {
+            if (groundLayer == null ||
+                    !groundLayer.isBackgroundLayer())
+                return;
 
-			new GameWindow(groundLayer);
-		}
+            new GameWindow(groundLayer);
+        }
 
-		public void updateEnabledState() {
-			if (frame == null) {
-				groundLayer = null;
-				setEnabled(false);
-				return;
-			}
+        public void updateEnabledState() {
+            if (frame == null) {
+                groundLayer = null;
+                setEnabled(false);
+                return;
+            }
 
-			if (currentLayer != null &&
-					currentLayer.isBackgroundLayer()) {
-				groundLayer = currentLayer;
-				setEnabled(true);
-				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;
-				}
+            /* 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);
-		}
-	}
+            groundLayer = null;
+            setEnabled(false);
+        }
+    }
 
-	protected DriveAction driveAction = new DriveAction();
+    protected DriveAction driveAction = new DriveAction();
 
-	public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
-		if (oldFrame != null)
-			oldFrame.mapView.removeLayerChangeListener(this);
+    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+        if (oldFrame != null)
+            oldFrame.mapView.removeLayerChangeListener(this);
 
-		driveAction.frame = newFrame;
-		driveAction.updateEnabledState();
+        driveAction.frame = newFrame;
+        driveAction.updateEnabledState();
 
-		if (newFrame != null)
-			newFrame.mapView.addLayerChangeListener(this);
-	}
+        if (newFrame != null)
+            newFrame.mapView.addLayerChangeListener(this);
+    }
 
-	/* LayerChangeListener methods */
-	public void activeLayerChange(Layer oldLayer, Layer newLayer) {
-		driveAction.currentLayer = newLayer;
-		driveAction.updateEnabledState();
-	}
+    /* LayerChangeListener methods */
+    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+        driveAction.currentLayer = newLayer;
+        driveAction.updateEnabledState();
+    }
 
-	public void layerAdded(Layer newLayer) {
-		driveAction.updateEnabledState();
-	}
+    public void layerAdded(Layer newLayer) {
+        driveAction.updateEnabledState();
+    }
 
-	public void layerRemoved(Layer oldLayer) {
-		driveAction.updateEnabledState();
-	}
+    public void layerRemoved(Layer oldLayer) {
+        driveAction.updateEnabledState();
+    }
 }
