gt: 		cvy = Math.min(cvy, maxY);
context: /* $Id$ */
  /***************************************************************************
   *                      (C) Copyright 2003 - Marauroa                      *
   ***************************************************************************
   ***************************************************************************
   *                                                                         *
   *   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.                                   *
   *                                                                         *
   ***************************************************************************/
  package games.stendhal.client;
  
  import games.stendhal.client.entity.Corpse;
  import games.stendhal.client.entity.IEntity;
  import games.stendhal.client.entity.Item;
  import games.stendhal.client.gui.DropTarget;
  import games.stendhal.client.gui.GroundContainer;
  import games.stendhal.client.gui.j2d.AchievementBoxFactory;
  import games.stendhal.client.gui.j2d.RemovableSprite;
  import games.stendhal.client.gui.j2d.entity.Entity2DView;
  import games.stendhal.client.gui.j2d.entity.EntityView;
  import games.stendhal.client.gui.spellcasting.SpellCastingGroundContainerMouseState;
  import games.stendhal.client.sprite.Sprite;
  import games.stendhal.client.sprite.SpriteStore;
  
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.Graphics;
  import java.awt.Graphics2D;
  import java.awt.GraphicsConfiguration;
  import java.awt.Point;
  import java.awt.Rectangle;
  import java.awt.RenderingHints;
  import java.awt.event.ComponentAdapter;
  import java.awt.event.ComponentEvent;
  import java.awt.event.KeyEvent;
  import java.awt.geom.Point2D;
  import java.awt.image.VolatileImage;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Map;
  
  import javax.swing.JComponent;
  import javax.swing.SwingUtilities;
  
  import marauroa.common.game.RPObject;
  import marauroa.common.game.RPSlot;
  
  import org.apache.log4j.Logger;
  
  /**
   * The game screen. This manages and renders the visual elements of the game.
   */
  public class GameScreen extends JComponent implements IGameScreen, DropTarget,
  	GameObjects.GameObjectListener, StendhalClient.ZoneChangeListener {
  	/**
  	 * serial version uid.
  	 */
  	private static final long serialVersionUID = -4070406295913030925L;
  
  	private static final Logger logger = Logger.getLogger(GameScreen.class);
  	
  	/** Map KeyEvents to a number, i.e. to determine position in spells slot based on pressed key **/
  	private static final Map<Integer, Integer> keyEventMapping = new HashMap<Integer, Integer>();
  	static {
  		keyEventMapping.put(KeyEvent.VK_1, Integer.valueOf(1));
  		keyEventMapping.put(KeyEvent.VK_2, Integer.valueOf(2));
  		keyEventMapping.put(KeyEvent.VK_3, Integer.valueOf(3));
  		keyEventMapping.put(KeyEvent.VK_4, Integer.valueOf(4));
  		keyEventMapping.put(KeyEvent.VK_5, Integer.valueOf(5));
  		keyEventMapping.put(KeyEvent.VK_6, Integer.valueOf(6));
  		keyEventMapping.put(KeyEvent.VK_7, Integer.valueOf(7));
  		keyEventMapping.put(KeyEvent.VK_8, Integer.valueOf(8));
  		keyEventMapping.put(KeyEvent.VK_9, Integer.valueOf(9));
  		keyEventMapping.put(KeyEvent.VK_0, Integer.valueOf(10));
  	}
  
  	/**
  	 * A scale factor for panning delta (to allow non-float precision).
  	 */
  	private static final int PAN_SCALE = 8;
  	/**
  	 * Speed factor for centering the screen. Smaller is faster,
  	 * and keeps the player closer to the center of the screen when walking.
  	 */
  	private static final int PAN_INERTIA = 15;
  	/**
  	 * Space at the right and bottom of the screen next to the off line
  	 * indicator icon.
  	 */
  	private static final int OFFLINE_MARGIN = 10;
  
  	private static final Sprite offlineIcon;
  
  	/** the singleton instance. */
  	private static GameScreen screen;
  
  	/**
  	 * Static game layers.
  	 */
  	private final StaticGameLayers gameLayers;
  	
  	/** Entity views container. */
  	private final EntityViewManager viewManager = new EntityViewManager();
  
  	/** Actual width of the world in world units. */
  	private int ww;
  	/** Actual height of the world in world units. */
  	private int wh;
  
  	/**
  	 * The ground layer.
  	 */
  	private final GroundContainer ground;
  
  	/**
  	 * The text bubbles.
  	 */
  	private final List<RemovableSprite> texts;
  	
  	/**
  	 * Text boxes that are anchored to the screen coordinates.
  	 */
  	private final List<RemovableSprite> staticSprites;
  
  	private boolean offline;
  
  	/**
  	 * Off line indicator counter. 
  	 */
  	private int blinkOffline;
  
  	/**
  	 * The targeted center of view X coordinate (truncated).
  	 */
  	private int x;
  
  	/**
  	 * The targeted center of view Y coordinate (truncated).
  	 */
  	private int y;
  
  	/** Actual size of the screen in pixels. */
  	private int sw;
  	private int sh;
  
  	/**
  	 * The difference between current and target screen view X.
  	 */
  	private int dvx;
  
  	/**
  	 * The difference between current and target screen view Y.
  	 */
  	private int dvy;
  
  	/**
  	 * The current screen view X.
  	 */
  	private int svx;
  
  	/**
  	 * The current screen view Y.
  	 */
  	private int svy;
  
  	/**
  	 * Current panning speed.
  	 */
  	private int speed;
  	
  	/**
  	 * Scaling factor of the screen.
  	 */
  	private double scale = 1.0;
  	/**
  	 * Flag for telling if the screen should be scaled if it's not of the
  	 * default size.
  	 */
  	private boolean useScaling = true;
  	/**
  	 * A flag for telling if the screen is being actually scaled. Used for
  	 * detecting if the ground layers will need triple buffering.
  	 */
  	private boolean useTripleBuffer;
  	/**
  	 * Buffer for drawing the ground layers when the screen is scaled.
  	 */
  	private VolatileImage buffer;
  
  	static {
  		offlineIcon = SpriteStore.get().getSprite("data/gui/offline.png");
  	}
  
  	private AchievementBoxFactory achievementBoxFactory;
  
  	/**
  	 * Create a game screen.
  	 *
  	 * @param client
  	 *            The client.
  	 */
  	public GameScreen(final StendhalClient client) {
  		setSize(stendhal.getDisplaySize());
  		addComponentListener(new ComponentAdapter() {
  			@Override
  			public void componentResized(ComponentEvent e) {
  				onResized();
  			}
  		});
  		
  		gameLayers = client.getStaticGameLayers();
  
  		sw = getWidth();
  		sh = getHeight();
  
  		x = 0;
  		y = 0;
  		svx = -sw / 2;
  		svy = -sh / 2;
  		dvx = 0;
  		dvy = 0;
  
  		speed = 0;
  
  		// Drawing is done in EDT
  		texts = Collections.synchronizedList(new LinkedList<RemovableSprite>());
  		staticSprites = Collections.synchronizedList(new LinkedList<RemovableSprite>());
  
  		// create ground
  		ground = new GroundContainer(client, this, this);
  
  		// register native event handler
  		addMouseListener(ground);
  		addMouseWheelListener(ground);
  		addMouseMotionListener(ground);
  		/*
  		 * Ignore OS level repaint requests to help systems that create too
  		 * many of those. In game DnD is done within AWT so that is not
  		 * affected.
  		 */
  		setIgnoreRepaint(true);
  		client.getGameObjects().addGameObjectListener(this);
  	}
  	
  	/**
  	 * The canvas can be resized using a split pane, or with window size changes
  	 * if screen scaling is used. This is for adjusting the internal parameters
  	 * for the change. 
  	 */
  	private void onResized() {
  		Dimension screenSize = stendhal.getDisplaySize();
  		sw = getWidth();
  		sh = getHeight();
  		if (useScaling) {
  			double xScale = sw / screenSize.getWidth();
  			double yScale = sh / screenSize.getHeight();
  			// Scale by the dimension that needs more scaling
  			scale = Math.max(xScale, yScale);
  			if (Math.abs(scale - 1.0) > 0.0001) {
  				useTripleBuffer = true;
  			} else {
  				useTripleBuffer = false;
  				buffer = null;
  			}
  		} else {
  			sw = Math.min(sw, screenSize.width);
  			sh = Math.min(sh, screenSize.height);
  			useTripleBuffer = false;
  			buffer = null;
  		}
  		// Reset the view so that the player is in the center
  		calculateView(x, y);
  		center();
  	}
  	
  	/**
  	 * Set whether the screen should be drawn scaled, or in native resolution.
  	 *  
  	 * @param useScaling if <code>true</code> the screen will scale the view
  	 * 	will be scaled to fit the screen size, otherwise it will be drawn using
  	 * 	the native resolution.
  	 */
  	public void setUseScaling(boolean useScaling) {
  		this.useScaling = useScaling;
  		if (!useScaling) {
  			scale = 1.0;
  			useTripleBuffer = false;
  			buffer = null;
  		} else {
  			onResized();
  		}
  	}
  	
  	/**
  	 * Check if the screen uses scaling. Note that if the native resolution
  	 * is in use, the screen size <b>must not</b> be allowed to grow larger than
  	 * <code>standhal.getScreenSize()</code>.
  	 * 
  	 * @return <code>true</code> if the graphics are scaled to the screen size,
  	 * 	<code>false</code> if the native resolution is used
  	 */
  	public boolean isScaled() {
  		return useScaling;
  	}
  
  	/**
  	 * Set the default [singleton] screen.
  	 *
  	 * @param screen
  	 *            The screen.
  	 */
  	public static void setDefaultScreen(final GameScreen screen) {
  		GameScreen.screen = screen;
  	}
  
  	/** @return the GameScreen object. */
  	public static GameScreen get() {
  		return screen;
  	}
  
  	/** @return screen width in world units. */
  	private int getViewWidth() {
  		return (int) Math.ceil(sw / (SIZE_UNIT_PIXELS * scale));
  	}
  
  	/** @return screen height in world units .*/
  	private int getViewHeight() {
  		return (int) Math.ceil(sh / (SIZE_UNIT_PIXELS * scale));
  	}
  	
  	/*
  	 * (non-Javadoc)
  	 *
  	 * @see games.stendhal.client.IGameScreen#nextFrame()
  	 */
  	@Override
  	public void nextFrame() {
  		adjustView();
  	}
  	
  	/**
  	 * Get the achievement box factory.
  	 * 
  	 * @return factory
  	 */
  	private AchievementBoxFactory getAchievementFactory() {
  		if (achievementBoxFactory == null) {
  			achievementBoxFactory = new AchievementBoxFactory();
  		}
  		return achievementBoxFactory;
  	}
  
  	/*
  	 * (non-Javadoc)
  	 *
  	 * @see games.stendhal.client.IGameScreen#addEntity(games.stendhal.client.entity.Entity)
  	 */
  	@Override
  	public void addEntity(final IEntity entity) {
  		EntityView<IEntity> view = viewManager.addEntity(entity);
  		if (view != null) {
  			if (view instanceof Entity2DView) {
  				final Entity2DView<?> inspectable = (Entity2DView<?>) view;
  				
  				inspectable.setInspector(ground);
  			}
  			if (entity.isUser()) {
  				SwingUtilities.invokeLater(new Runnable() {
  					@Override
  					public void run() {
  						center();
  					}
  				});
  			}
  		}
  	}
  
  	/*
  	 * (non-Javadoc)
  	 *
  	 * @see games.stendhal.client.IGameScreen#removeEntity(games.stendhal.client.entity.Entity)
  	 */
  	@Override
  	public void removeEntity(final IEntity entity) {
  		viewManager.removeEntity(entity);
  	}
  
  	/**
  	 * Update the view position to center the target position.
  	 */
  	private void adjustView() {
  		/*
  		 * Already centered?
  		 */
  		if ((dvx == 0) && (dvy == 0)) {
  			return;
  		}
  
  		final int sx = convertWorldXToScaledScreen(x) - getScreenViewX() + SIZE_UNIT_PIXELS / 2;
  		final int sy = convertWorldYToScaledScreen(y) - getScreenViewY() + SIZE_UNIT_PIXELS / 2;
  
  		if ((sx < 0) || (sx >= sw) || (sy < -SIZE_UNIT_PIXELS) || (sy > sh)) {
  			/*
  			 * If off screen, just center
  			 */
  			center();
  		} else {
  			calculatePanningSpeed();
  
  			/*
  			 * Moving?
  			 */
  			if (speed != 0) {
  				/*
  				 * Not a^2 + b^2 = c^2, but good enough
  				 */
  				final int scalediv = (Math.abs(dvx) + Math.abs(dvy)) * PAN_SCALE;
  
  				int dx = speed * dvx / scalediv;
  				int dy = speed * dvy / scalediv;
  
  				/*
  				 * Don't overshoot. Don't stall.
  				 */
  				if (dvx < 0) {
  					dx = Math.max(Math.min(-1, dx), dvx);
  				} else if (dvx > 0) {
  					dx = Math.min(Math.max(1, dx), dvx);
  				}
  
  				if (dvy < 0) {
  					dy = Math.max(Math.min(-1, dy), dvy);
  				} else if (dvy > 0) {
  					dy = Math.min(Math.max(1, dy), dvy);
  				}
  
  				/*
  				 * Adjust view
  				 */
  				svx += dx;
  				dvx -= dx;
  
  				svy += dy;
  				dvy -= dy;
  			}
  		}
  	}
  	
  	/**
  	 * Calculate the target speed for moving the view position. The farther
  	 * away, the faster.
  	 */
  	private void calculatePanningSpeed() {
  		final int dux = dvx / PAN_INERTIA;
  		final int duy = dvy / PAN_INERTIA;
  
  		final int tspeed = ((dux * dux) + (duy * duy)) * PAN_SCALE;
  
  		if (speed > tspeed) {
  			speed = (2 * speed + tspeed) / 3;
  
  			/*
  			 * Don't stall
  			 */
  			if ((dvx != 0) || (dvy != 0)) {
  				speed = Math.max(speed, 1);
  			}
  		} else if (speed < tspeed) {
  			speed += 2;
  		}
  	}
  
  	/**
  	 * Updates the target position of the view center.
  	 * Prefer top left if the map is smaller than the screen.
  	 * 
  	 * @param x preferred x of center, if the map is large enough
  	 * @param y preferred y of center, if the map is large enough
  	 */
  	private void calculateView(int x, int y) {
  		// Coordinates for a screen centered on player
  		int cvx = (int) ((x * SIZE_UNIT_PIXELS) + (SIZE_UNIT_PIXELS / 2) - (sw / 2) / scale);
  		int cvy = (int) ((y * SIZE_UNIT_PIXELS) + (SIZE_UNIT_PIXELS / 2) - (sh / 2) / scale);
  
  		/*
  		 * Keep the world within the screen view
  		 */
  		final int maxX = (int) (ww * SIZE_UNIT_PIXELS - sw / scale);
  		cvx = Math.min(cvx, maxX);
  		cvx = Math.max(cvx, 0);
  		
  		final int maxY = (int) (wh * SIZE_UNIT_PIXELS - sh / scale);
  