    protected void renderTokens(Graphics2D g, List<Token> tokenList) {
    	Grid grid = zone.getGrid();
    	Dimension screenSize = getSize();
        int scaledGridWidth = (int)(grid.getCellWidth()*getScale());
        int scaledGridHeight = (int)(grid.getCellHeight()*getScale());
        Rectangle clipBounds = g.getClipBounds();
        float scale = zoneScale.getScale();
        double gridSize = getScaledGridSize();
        for (Token token : tokenList) {
        	if (!zone.isTokenVisible(token) && !AppUtil.playerOwns(token)) {
        		continue;
        	}
            int width = (int)Math.ceil(TokenSize.getWidth(token, zone.getGrid()) * scale);
            int height = (int)Math.ceil(TokenSize.getHeight(token, zone.getGrid()) * scale);
            if (!token.isStamp()) {
            	width --;
            	height --;
            }
            ScreenPoint tokenScreenLocation = ScreenPoint.fromZonePoint(this, token.getX(), token.getY());
            int x = tokenScreenLocation.x + 1 + (int)(grid.getCellOffset().width*scale);
            int y = tokenScreenLocation.y + 1 + (int)(grid.getCellOffset().height*scale);
            if (width < scaledGridWidth && token.isSnapToGrid()) {
                x += (scaledGridWidth - width)/2;
            }
            if (height < scaledGridHeight && token.isSnapToGrid()) {
                y += (scaledGridHeight - height)/2;
            }
            Rectangle tokenBounds = new Rectangle(x, y, width, height);
            if (x+width < 0 || x > screenSize.width || y+height < 0 || y > screenSize.height) {
            	continue;
            }
            if (!token.isStamp() && !token.isBackground()) {
	            for (TokenLocation location : getTokenLocations(Zone.Layer.TOKEN)) {
	            	Rectangle r1 = location.bounds;
	            	if (tokenBounds.intersects(r1)) {
	            		Rectangle oldRect = null;
	            		for (Rectangle r2 : coveredTokenSet) {
	            			if (tokenBounds.contains(r2)) {
	            				oldRect = r2;
	            				break;
	            			}
	            		}
	            		if (oldRect != null) {
	            			coveredTokenSet.remove(oldRect);
	            		}
	            		coveredTokenSet.add(tokenBounds);
	            	}
	            }
            }
            List<TokenLocation> locationList = null;
            if (!token.isStamp() && !token.isBackground()) {
            	locationList = getTokenLocations(Zone.Layer.TOKEN);
            } else {
            	if (token.isStamp()) {
            		locationList = getTokenLocations(Zone.Layer.STAMP);
            	} 
            	if (token.isBackground()) {
            		locationList = getTokenLocations(Zone.Layer.BACKGROUND);
            	}
            }
            if (locationList != null) {
            	locationList.add(new TokenLocation(tokenBounds, token));
            }
			BufferedImage image = null;
            Asset asset = AssetManager.getAsset(token.getAssetID());
            if (asset == null) {
                MapTool.serverCommand().getAsset(token.getAssetID());
                image = ImageManager.UNKNOWN_IMAGE;
            } else {
				image = ImageManager.getImage(AssetManager.getAsset(token.getAssetID()), this);
            }
            if (!tokenBounds.intersects(clipBounds)) {
                continue;
            }
			if (isTokenMoving(token)) {
				BufferedImage replacementImage = replacementImageMap.get(token);
				if (replacementImage == null) {
					replacementImage = ImageUtil.rgbToGrayscale(image);
					replacementImageMap.put(token, replacementImage);
				}
				image = replacementImage;
			}
			if (showPathList.contains(token) && token.getLastPath() != null) {
				renderPath(g, token.getLastPath(), (int)(width/gridSize), (int)(height/gridSize));
			}
			if (token.hasFacing() && (token.getTokenType() == Token.Type.TOP_DOWN || token.isStamp() || token.isBackground())) {
				AffineTransform at = new AffineTransform();
				at.translate(x, y);
				at.rotate(Math.toRadians(-token.getFacing() - 90), width/2, height/2); 
				if (token.isSnapToScale()) {
					at.scale((double)TokenSize.getWidth(token, zone.getGrid()) / token.getWidth(), (double)TokenSize.getHeight(token, zone.getGrid()) / token.getHeight());
				} else {
					at.scale((double) token.getWidth() / image.getWidth(), (double) token.getHeight() / image.getHeight());
				}
				at.scale(getScale(), getScale());
	            g.drawImage(image, at, this);
			} else {
	            g.drawImage(image, x, y, width, height, this);
			}
			if (token.hasFacing()) {
				Token.Type tokenType = token.getTokenType();
				switch(tokenType) {
				case CIRCLE:
					int size = TokenSize.getWidth(token, zone.getGrid())/2;
					Shape arrow = getCircleFacingArrow(token.getFacing(), size);
					int cx = x + width/2;
					int cy = y + height/2;
					g.translate(cx, cy);
					g.setColor(Color.yellow);
					g.fill(arrow);
					g.setColor(Color.darkGray);
					g.draw(arrow);
					g.translate(-cx, -cy);
					break;
				case SQUARE:
					size = TokenSize.getWidth(token, zone.getGrid())/2;
					int facing = token.getFacing();
					arrow = getSquareFacingArrow(facing, size);
					cx = x + width/2;
					cy = y + height/2;
					int xp = width/2;
					int yp = height/2;
					if (facing >= 45 && facing <= 135 || facing <= -45 && facing >= -135) {
						xp = (int)(yp / Math.tan(Math.toRadians(facing)));
						if (facing < 0 ) {
							xp = -xp;
							yp = -yp;
						}
					} else {
						yp = (int)(xp * Math.tan(Math.toRadians(facing)));
						if (facing > 135 || facing < -135) {
							xp = -xp;
							yp = -yp;
						}
					}
					cx += xp;
					cy -= yp;
					g.translate(cx, cy);
					g.setColor(Color.yellow);
					g.fill(arrow);
					g.setColor(Color.darkGray);
					g.draw(arrow);
					g.translate(-cx, -cy);
					break;
				}
			}
            if (!token.getStatePropertyNames().isEmpty()) {
              Shape clip = g.getClip();
              g.translate(x, y);
              Rectangle bounds = new Rectangle(0, 0, width, height);
              Rectangle overlayClip = g.getClipBounds().intersection(bounds);
              g.setClip(overlayClip);
              for (String state : token.getStatePropertyNames()) {
                Object stateValue = token.getState(state);
                if (stateValue instanceof Boolean && ((Boolean)stateValue).booleanValue()) {
                  TokenOverlay overlay =  TokenStates.getOverlay(state);
                  if (overlay != null) overlay.paintOverlay(g, token, bounds);
                } else if (stateValue instanceof TokenOverlay) {
                  ((TokenOverlay)stateValue).paintOverlay(g, token, bounds);
                } 
              } 
              g.translate(-x, -y);
              g.setClip(clip);
            } 
        }
        for (TokenLocation location : getTokenLocations(getActiveLayer())) {
        	Rectangle bounds = location.bounds;
            if (!bounds.intersects(clipBounds)) {
                continue;
            }
        	Token token = location.token;
        	boolean isSelected = selectedTokenSet.contains(token.getId());
        	if (isSelected) {
        		ImageBorder selectedBorder = token.isStamp() || token.isBackground() ? AppStyle.selectedStampBorder : AppStyle.selectedBorder;
    			if (token.hasFacing() && (token.getTokenType() == Token.Type.TOP_DOWN || token.isStamp() || token.isBackground())) {
    				AffineTransform oldTransform = g.getTransform();
    				g.translate(bounds.x, bounds.y);
    				g.rotate(Math.toRadians(-token.getFacing() - 90), bounds.width/2, bounds.height/2); 
    				selectedBorder.paintAround(g, 0, 0, bounds.width, bounds.height);
    				g.setTransform(oldTransform);
    			} else {
    				selectedBorder.paintAround(g, bounds);
    			}
        	}
        	if (AppState.isShowTokenNames() || isSelected || token == tokenUnderMouse) {
        		String name = token.getName();
        		if (MapTool.getPlayer().isGM() && token.getGMName() != null && token.getGMName().length() > 0) {
        			name += " (" + token.getGMName() + ")";
        		}
                GraphicsUtil.drawBoxedString(g, name, bounds.x + bounds.width/2, bounds.y + bounds.height + 10);
            }
        }
        for (Rectangle rect : coveredTokenSet) {
        	BufferedImage stackImage = AppStyle.stackImage;
        	g.drawImage(stackImage, rect.x + rect.width - stackImage.getWidth() + 2, rect.y - 2, null);
        }
    }

    protected void renderTokens(Graphics2D g, List<Token> tokenList) {
    	Grid grid = zone.getGrid();
    	Dimension screenSize = getSize();
        int scaledGridWidth = (int)(grid.getCellWidth()*getScale());
        int scaledGridHeight = (int)(grid.getCellHeight()*getScale());
        Rectangle clipBounds = g.getClipBounds();
        float scale = zoneScale.getScale();
        double gridSize = getScaledGridSize();
        for (Token token : tokenList) {
        	if (!zone.isTokenVisible(token) && !MapTool.getPlayer().isGM()) {
        		continue;
        	}
            int width = (int)Math.ceil(TokenSize.getWidth(token, zone.getGrid()) * scale);
            int height = (int)Math.ceil(TokenSize.getHeight(token, zone.getGrid()) * scale);
            if (!token.isStamp()) {
            	width --;
            	height --;
            }
            ScreenPoint tokenScreenLocation = ScreenPoint.fromZonePoint(this, token.getX(), token.getY());
            int x = tokenScreenLocation.x + 1 + (int)(grid.getCellOffset().width*scale);
            int y = tokenScreenLocation.y + 1 + (int)(grid.getCellOffset().height*scale);
            if (width < scaledGridWidth && token.isSnapToGrid()) {
                x += (scaledGridWidth - width)/2;
            }
            if (height < scaledGridHeight && token.isSnapToGrid()) {
                y += (scaledGridHeight - height)/2;
            }
            Rectangle tokenBounds = new Rectangle(x, y, width, height);
            if (x+width < 0 || x > screenSize.width || y+height < 0 || y > screenSize.height) {
            	continue;
            }
            if (!token.isStamp() && !token.isBackground()) {
	            for (TokenLocation location : getTokenLocations(Zone.Layer.TOKEN)) {
	            	Rectangle r1 = location.bounds;
	            	if (tokenBounds.intersects(r1)) {
	            		Rectangle oldRect = null;
	            		for (Rectangle r2 : coveredTokenSet) {
	            			if (tokenBounds.contains(r2)) {
	            				oldRect = r2;
	            				break;
	            			}
	            		}
	            		if (oldRect != null) {
	            			coveredTokenSet.remove(oldRect);
	            		}
	            		coveredTokenSet.add(tokenBounds);
	            	}
	            }
            }
            List<TokenLocation> locationList = null;
            if (!token.isStamp() && !token.isBackground()) {
            	locationList = getTokenLocations(Zone.Layer.TOKEN);
            } else {
            	if (token.isStamp()) {
            		locationList = getTokenLocations(Zone.Layer.STAMP);
            	} 
            	if (token.isBackground()) {
            		locationList = getTokenLocations(Zone.Layer.BACKGROUND);
            	}
            }
            if (locationList != null) {
            	locationList.add(new TokenLocation(tokenBounds, token));
            }
			BufferedImage image = null;
            Asset asset = AssetManager.getAsset(token.getAssetID());
            if (asset == null) {
                MapTool.serverCommand().getAsset(token.getAssetID());
                image = ImageManager.UNKNOWN_IMAGE;
            } else {
				image = ImageManager.getImage(AssetManager.getAsset(token.getAssetID()), this);
            }
            if (!tokenBounds.intersects(clipBounds)) {
                continue;
            }
			if (isTokenMoving(token)) {
				BufferedImage replacementImage = replacementImageMap.get(token);
				if (replacementImage == null) {
					replacementImage = ImageUtil.rgbToGrayscale(image);
					replacementImageMap.put(token, replacementImage);
				}
				image = replacementImage;
			}
			if (showPathList.contains(token) && token.getLastPath() != null) {
				renderPath(g, token.getLastPath(), (int)(width/gridSize), (int)(height/gridSize));
			}
			if (token.hasFacing() && (token.getTokenType() == Token.Type.TOP_DOWN || token.isStamp() || token.isBackground())) {
				AffineTransform at = new AffineTransform();
				at.translate(x, y);
				at.rotate(Math.toRadians(-token.getFacing() - 90), width/2, height/2); 
				if (token.isSnapToScale()) {
					at.scale((double)TokenSize.getWidth(token, zone.getGrid()) / token.getWidth(), (double)TokenSize.getHeight(token, zone.getGrid()) / token.getHeight());
				} else {
					at.scale((double) token.getWidth() / image.getWidth(), (double) token.getHeight() / image.getHeight());
				}
				at.scale(getScale(), getScale());
	            g.drawImage(image, at, this);
			} else {
	            g.drawImage(image, x, y, width, height, this);
			}
			if (token.hasFacing()) {
				Token.Type tokenType = token.getTokenType();
				switch(tokenType) {
				case CIRCLE:
					int size = TokenSize.getWidth(token, zone.getGrid())/2;
					Shape arrow = getCircleFacingArrow(token.getFacing(), size);
					int cx = x + width/2;
					int cy = y + height/2;
					g.translate(cx, cy);
					g.setColor(Color.yellow);
					g.fill(arrow);
					g.setColor(Color.darkGray);
					g.draw(arrow);
					g.translate(-cx, -cy);
					break;
				case SQUARE:
					size = TokenSize.getWidth(token, zone.getGrid())/2;
					int facing = token.getFacing();
					arrow = getSquareFacingArrow(facing, size);
					cx = x + width/2;
					cy = y + height/2;
					int xp = width/2;
					int yp = height/2;
					if (facing >= 45 && facing <= 135 || facing <= -45 && facing >= -135) {
						xp = (int)(yp / Math.tan(Math.toRadians(facing)));
						if (facing < 0 ) {
							xp = -xp;
							yp = -yp;
						}
					} else {
						yp = (int)(xp * Math.tan(Math.toRadians(facing)));
						if (facing > 135 || facing < -135) {
							xp = -xp;
							yp = -yp;
						}
					}
					cx += xp;
					cy -= yp;
					g.translate(cx, cy);
					g.setColor(Color.yellow);
					g.fill(arrow);
					g.setColor(Color.darkGray);
					g.draw(arrow);
					g.translate(-cx, -cy);
					break;
				}
			}
            if (!token.getStatePropertyNames().isEmpty()) {
              Shape clip = g.getClip();
              g.translate(x, y);
              Rectangle bounds = new Rectangle(0, 0, width, height);
              Rectangle overlayClip = g.getClipBounds().intersection(bounds);
              g.setClip(overlayClip);
              for (String state : token.getStatePropertyNames()) {
                Object stateValue = token.getState(state);
                if (stateValue instanceof Boolean && ((Boolean)stateValue).booleanValue()) {
                  TokenOverlay overlay =  TokenStates.getOverlay(state);
                  if (overlay != null) overlay.paintOverlay(g, token, bounds);
                } else if (stateValue instanceof TokenOverlay) {
                  ((TokenOverlay)stateValue).paintOverlay(g, token, bounds);
                } 
              } 
              g.translate(-x, -y);
              g.setClip(clip);
            } 
        }
        for (TokenLocation location : getTokenLocations(getActiveLayer())) {
        	Rectangle bounds = location.bounds;
            if (!bounds.intersects(clipBounds)) {
                continue;
            }
        	Token token = location.token;
        	boolean isSelected = selectedTokenSet.contains(token.getId());
        	if (isSelected) {
        		ImageBorder selectedBorder = token.isStamp() || token.isBackground() ? AppStyle.selectedStampBorder : AppStyle.selectedBorder;
    			if (token.hasFacing() && (token.getTokenType() == Token.Type.TOP_DOWN || token.isStamp() || token.isBackground())) {
    				AffineTransform oldTransform = g.getTransform();
    				g.translate(bounds.x, bounds.y);
    				g.rotate(Math.toRadians(-token.getFacing() - 90), bounds.width/2, bounds.height/2); 
    				selectedBorder.paintAround(g, 0, 0, bounds.width, bounds.height);
    				g.setTransform(oldTransform);
    			} else {
    				selectedBorder.paintAround(g, bounds);
    			}
        	}
        	if (AppState.isShowTokenNames() || isSelected || token == tokenUnderMouse) {
        		String name = token.getName();
        		if (MapTool.getPlayer().isGM() && token.getGMName() != null && token.getGMName().length() > 0) {
        			name += " (" + token.getGMName() + ")";
        		}
                GraphicsUtil.drawBoxedString(g, name, bounds.x + bounds.width/2, bounds.y + bounds.height + 10);
            }
        }
        for (Rectangle rect : coveredTokenSet) {
        	BufferedImage stackImage = AppStyle.stackImage;
        	g.drawImage(stackImage, rect.x + rect.width - stackImage.getWidth() + 2, rect.y - 2, null);
        }
    }
