1 /** 2 * The chess framework library. 3 * More information is available at http://www.jinchess.com/. 4 * Copyright (C) 2002 Alexander Maryanovsky. 5 * All rights reserved. 6 * 7 * The chess framework library is free software; you can redistribute 8 * it and/or modify it under the terms of the GNU Lesser General Public License 9 * as published by the Free Software Foundation; either version 2 of the 10 * License, or (at your option) any later version. 11 * 12 * The chess framework library is distributed in the hope that it will 13 * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 15 * General Public License for more details. 16 * 17 * You should have received a copy of the GNU Lesser General Public License 18 * along with the chess framework library; if not, write to the Free Software 19 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 */ 21 22 package free.chess; 23 24 import java.awt.*; 25 import java.awt.image.FilteredImageSource; 26 import java.awt.image.ImageFilter; 27 import java.awt.image.RGBImageFilter; 28 import java.io.IOException; 29 import java.net.URL; 30 import java.util.*; 31 32 import free.util.IOUtilities; 33 import free.util.ImageUtilities; 34 import free.util.TextUtilities; 35 36 37 /** 38 * An implementation of <code>PiecePainter</code> which paints images. 39 */ 40 41 public final class ImagePiecePainter implements ResourcePiecePainter{ 42 43 44 45 /** 46 * The painter we delegate to while loading images. 47 */ 48 49 private static final PiecePainter whileLoadingDelegate = new DefaultPiecePainter(); 50 51 52 53 /** 54 * The ImageFilter we use to create shaded images. 55 */ 56 57 private static final ImageFilter SHADING_FILTER = new ShadingFilter(); 58 59 60 61 /** 62 * True if piece images are to be loaded asynchronously, and in the meanwhile, 63 * the delegate should be used. 64 */ 65 66 private static volatile boolean asyncImageLoad = false; 67 68 69 70 /** 71 * An array whose indices specify the size of the images and whose values 72 * are maps mapping <code>Piece</code>s to either <code>Image</code>s or 73 * <code>URL</code>s, if the image isn't loaded yet. 74 */ 75 76 private Map [] pieceImages; 77 78 79 80 /** 81 * Same as pieceImages only for shaded images. 82 */ 83 84 private Map [] shadedPieceImages; 85 86 87 88 /** 89 * Maps the square sizes for which images are currently being loaded to the 90 * <code>ImageDataReceiver</code>s waiting on the data. 91 */ 92 93 private final Map imageDataReceivers = new HashMap(2); 94 95 96 97 /** 98 * A no-arg constructor, so that this piece painter can be used as a 99 * <code>ResourcePiecePainter</code>. 100 */ 101 ImagePiecePainter()102 public ImagePiecePainter(){ 103 104 } 105 106 107 108 /** 109 * Creates a new <code>ImagePiecePainter</code> with the specified piece 110 * images. The given <code>Map</code> should map <code>Integer</code>s 111 * specifying the size of the piece images to <code>Maps</code>s which in 112 * turn map <code>Piece</code>s to piece <code>Image</code>s. 113 */ 114 ImagePiecePainter(Map pieceImages)115 public ImagePiecePainter(Map pieceImages){ 116 117 // Find the largest size 118 int maxSize = 0; 119 Iterator sizes = pieceImages.keySet().iterator(); 120 while (sizes.hasNext()){ 121 int size = ((Integer)sizes.next()).intValue(); 122 if (size <= 0) 123 throw new IllegalArgumentException("Image sizes must be positive"); 124 125 if (size > maxSize) 126 maxSize = size; 127 } 128 129 if (maxSize == 0) 130 throw new IllegalArgumentException("No sizes in the hashtable"); 131 132 this.pieceImages = new Map[maxSize + 1]; 133 this.shadedPieceImages = new Map[maxSize + 1]; 134 135 // Fill the array 136 sizes = pieceImages.keySet().iterator(); 137 while (sizes.hasNext()){ 138 Integer size = (Integer)sizes.next(); 139 int sizeInt = size.intValue(); 140 141 Map images = (Map)pieceImages.get(size); 142 int imagesCount = images.size(); 143 this.pieceImages[sizeInt] = new HashMap(imagesCount); 144 this.shadedPieceImages[sizeInt] = new HashMap(imagesCount); 145 146 Iterator pieces = images.keySet().iterator(); 147 while (pieces.hasNext()){ 148 Object key = pieces.next(); 149 Image image = (Image)images.get(key); 150 Image shadedImage = shadeImage(image); 151 152 this.pieceImages[sizeInt].put(key, image); 153 this.shadedPieceImages[sizeInt].put(key, shadedImage); 154 } 155 } 156 } 157 158 159 160 /** 161 * Sets whether piece images are to be loaded asynchronously and a simpler 162 * piece painter delegate used while they load. Asynchronous loading is off 163 * by default. 164 */ 165 setAsyncImageLoad(boolean asyncImageLoad)166 public static void setAsyncImageLoad(boolean asyncImageLoad){ 167 ImagePiecePainter.asyncImageLoad = asyncImageLoad; 168 } 169 170 171 172 /** 173 * Creates a shaded version of the specified image. 174 */ 175 shadeImage(Image image)176 private static Image shadeImage(Image image){ 177 return Toolkit.getDefaultToolkit().createImage(new FilteredImageSource(image.getSource(), SHADING_FILTER)); 178 } 179 180 181 182 /** 183 * Since <code>ImagePiecePainter</code>s are immutable, simply returns 184 * <code>this</code>. 185 */ 186 freshInstance()187 public PiecePainter freshInstance(){ 188 return this; 189 } 190 191 192 193 /** 194 * Loads the piece images from the specified URL. The structure at the 195 * specified url is described below. 196 * A properties file named "definition" must be located at the base URL. 197 * That file should contain the following three properties: 198 * <ul> 199 * <li><code>image.type</code>: Specifies the extension (type) of the 200 * images - gif, png etc. If this is not specified, "gif" is assumed. 201 * <li><code>size.pref</code>: Specifies the preferred size of the piece 202 * set, in pixels. 203 * <li><code>size.list</code>: A list of sizes of all the available piece 204 * images, in increasing order, separated by spaces. 205 * </ul> 206 * Directories with names corresponding to the sizes must be present at the 207 * base URL and in those directories resources named 208 * <code>[color char][piece char].[extension]</code> where 209 * <code>[color char]</code> is either 'w' or 'b' (for black or white), 210 * <code>[piece char]</code> is one of 'k', 'q', 'r', 'b', 'n' 211 * or 'p'. The images for all twelve pieces must be present there and they 212 * must have the correct size. 213 */ 214 load(URL baseUrl)215 public void load(URL baseUrl) throws IOException{ 216 if (pieceImages != null) 217 throw new IllegalStateException("This ImagePiecePainter has already been loaded"); 218 219 URL defURL = new URL(baseUrl, "definition"); 220 Properties def = IOUtilities.loadProperties(defURL, true); 221 if (def == null) 222 throw new IOException("Unable to load " + defURL); 223 224 String ext = def.getProperty("ext", "gif"); 225 int [] sizes = TextUtilities.parseIntList(def.getProperty("size.list"), " "); 226 227 this.pieceImages = new HashMap[sizes[sizes.length - 1] + 1]; 228 this.shadedPieceImages = new HashMap[sizes[sizes.length - 1] + 1]; 229 230 Piece [] pieces = new Piece[]{ 231 ChessPiece.WHITE_KING, ChessPiece.BLACK_KING, 232 ChessPiece.WHITE_QUEEN, ChessPiece.BLACK_QUEEN, 233 ChessPiece.WHITE_ROOK, ChessPiece.BLACK_ROOK, 234 ChessPiece.WHITE_BISHOP, ChessPiece.BLACK_BISHOP, 235 ChessPiece.WHITE_KNIGHT, ChessPiece.BLACK_KNIGHT, 236 ChessPiece.WHITE_PAWN, ChessPiece.BLACK_PAWN}; 237 238 String [] pieceNames = 239 new String[]{"wk", "bk", "wq", "bq", "wr", "br", "wb", "bb", "wn", "bn", "wp", "bp"}; 240 241 for (int i = 0; i < sizes.length; i++){ 242 int size = sizes[i]; 243 244 Map normal = new HashMap(15); 245 Map shaded = new HashMap(15); 246 247 for (int j = 0; j < pieces.length; j++){ 248 URL imageUrl = new URL(baseUrl, size + "/" + pieceNames[j] + "." + ext); 249 normal.put(pieces[j], imageUrl); 250 shaded.put(pieces[j], imageUrl); 251 } 252 253 this.pieceImages[size] = normal; 254 this.shadedPieceImages[size] = shaded; 255 } 256 } 257 258 259 260 /** 261 * Returns the size of provided images which is the best fit for the 262 * specified square size. 263 */ 264 bestFitImageSize(int squareSize)265 protected int bestFitImageSize(int squareSize){ 266 if (squareSize <= 0) 267 throw new IllegalArgumentException("Image size must be positive"); 268 269 if (squareSize >= pieceImages.length) 270 return pieceImages.length - 1; 271 272 if (pieceImages[squareSize] != null) 273 return squareSize; 274 275 for (int i = squareSize; i > 0; i--) 276 if (pieceImages[i] != null) 277 return i; 278 279 for (int i = squareSize+1; i < pieceImages.length; i++) 280 if (pieceImages[i] != null) 281 return i; 282 283 throw new Error("This can't happen"); 284 } 285 286 287 288 /** 289 * If already loaded, returns the piece images at the specified size. 290 * Otherwise, starts loading them (if async loading is enabled, in a 291 * background thread, in the meanwhile, returning <code>null</code>), 292 * and once done, repaints the specified component. 293 */ 294 loadPieces(int squareSize, boolean shaded, Component target)295 protected synchronized Map loadPieces(int squareSize, boolean shaded, Component target){ 296 int imageSize = bestFitImageSize(squareSize); 297 Map images = (shaded ? shadedPieceImages : pieceImages)[imageSize]; 298 if (images.values().iterator().next() instanceof Image) // Already loaded 299 return images; 300 else{ 301 ImageDataReceiver receiver = (ImageDataReceiver)imageDataReceivers.get(new Integer(imageSize)); 302 if (receiver != null){ // We're already loading the images 303 receiver.addComponentToRepaint(target); 304 return null; 305 } 306 307 Set entrySet = images.entrySet(); 308 Piece [] pieces = new Piece[entrySet.size()]; 309 URL [] urls = new URL[entrySet.size()]; 310 Iterator entries = entrySet.iterator(); 311 for (int i = 0; i < pieces.length; i++){ 312 Map.Entry entry = (Map.Entry)entries.next(); 313 pieces[i] = (Piece)entry.getKey(); 314 urls[i] = (URL)entry.getValue(); 315 } 316 317 Map normalImages = pieceImages[imageSize]; 318 Map shadedImages = shadedPieceImages[imageSize]; 319 receiver = new ImageDataReceiver(asyncImageLoad ? target : null, imageSize, normalImages, shadedImages); 320 imageDataReceivers.put(new Integer(imageSize), receiver); 321 322 if (asyncImageLoad){ 323 IOUtilities.loadAsynchronously(urls, pieces, receiver, true); 324 return null; 325 } 326 else{ 327 IOUtilities.loadSynchronously(urls, pieces, receiver, true); 328 return shaded ? shadedImages : normalImages; 329 } 330 } 331 } 332 333 334 335 336 /** 337 * Paints the image of the given piece at the given coordinates on the given 338 * Graphics object scaled to the given size. 339 */ 340 paintPiece(Piece piece, Graphics g, Component component, Rectangle rect, boolean shaded)341 public void paintPiece(Piece piece, Graphics g, Component component, Rectangle rect, 342 boolean shaded){ 343 344 int x = rect.x; 345 int y = rect.y; 346 int width = rect.width; 347 int height = rect.height; 348 349 int size = width > height ? height : width; 350 351 Map pieces = loadPieces(size, shaded, component); 352 if (pieces == null){ 353 whileLoadingDelegate.paintPiece(piece, g, component, rect, shaded); 354 return; 355 } 356 357 Image pieceImage = (Image)pieces.get(piece); 358 int pieceWidth = pieceImage.getWidth(null); 359 int pieceHeight = pieceImage.getHeight(null); 360 361 g.drawImage(pieceImage, x + (width - pieceWidth)/2, y + (height - pieceHeight)/2, component); 362 } 363 364 365 366 367 /** 368 * The <code>ImageFilter</code> we use to create shaded piece images. 369 */ 370 371 private static class ShadingFilter extends RGBImageFilter{ 372 filterRGB(int x, int y, int rgb)373 public int filterRGB(int x, int y, int rgb){ 374 int alpha = (rgb >> 24) & 0xff; 375 int r = (rgb >> 16) & 0xff; 376 int g = (rgb >> 8) & 0xff; 377 int b = rgb & 0xff; 378 379 r = (r + 128*2)/3; 380 g = (g + 128*2)/3; 381 b = (b + 128*2)/3; 382 383 return (alpha << 24) | (r << 16) | (g << 8) | b; 384 } 385 386 } 387 388 389 390 /** 391 * The receiver of loaded image data. Responsible for creating and correctly 392 * mapping the piece images. 393 */ 394 395 private class ImageDataReceiver implements IOUtilities.DataReceiver{ 396 397 398 399 /** 400 * The map of pieces to normal images. 401 */ 402 403 private final Map normalImages; 404 405 406 407 /** 408 * The map of pieces to shaded images. 409 */ 410 411 private final Map shadedImages; 412 413 414 415 /** 416 * The size of the images we're loading. 417 */ 418 419 private final int imageSize; 420 421 422 423 /** 424 * The components to repaint when the loading is done. 425 */ 426 427 private final Set componentsToRepaint = new HashSet(2); 428 429 430 431 /** 432 * Creates a new <code>ImageDataReceiver</code> with the specified maps to 433 * fill and component to repaint when loading is done. 434 */ 435 ImageDataReceiver(Component componentToRepaint, int imageSize, Map normalImages, Map shadedImages)436 public ImageDataReceiver(Component componentToRepaint, int imageSize, Map normalImages, Map shadedImages){ 437 this.normalImages = normalImages; 438 this.shadedImages = shadedImages; 439 this.imageSize = imageSize; 440 componentsToRepaint.add(componentToRepaint); 441 } 442 443 444 445 /** 446 * Adds a component to the set of components to repaint once all the images 447 * are loaded. 448 */ 449 addComponentToRepaint(Component component)450 public void addComponentToRepaint(Component component){ 451 componentsToRepaint.add(component); 452 } 453 454 455 456 /** 457 * Called when the image data has been loaded. Creates and maps the piece 458 * images. 459 */ 460 dataRead(URL [] urls, Object id, byte [][] data, IOException[] exceptions)461 public void dataRead(URL [] urls, Object id, byte [][] data, IOException[] exceptions){ 462 // If there are any exceptions, we simply quit - this will cause 463 // the painter to keep using the delegate to paint pieces. 464 for (int i = 0; i < exceptions.length; i++) 465 if (exceptions[i] != null) 466 return; 467 468 synchronized(ImagePiecePainter.this){ 469 Toolkit toolkit = Toolkit.getDefaultToolkit(); 470 Piece [] pieces = (Piece[])id; 471 for (int i = 0; i < data.length; i++){ 472 Image normalImage = toolkit.createImage(data[i]); 473 Image shadedImage = shadeImage(normalImage); 474 475 ImageUtilities.preload(normalImage); 476 ImageUtilities.preload(shadedImage); 477 478 normalImages.put(pieces[i], normalImage); 479 shadedImages.put(pieces[i], shadedImage); 480 } 481 482 imageDataReceivers.remove(new Integer(imageSize)); 483 484 for (Iterator i = componentsToRepaint.iterator(); i.hasNext();){ 485 Component component = (Component)i.next(); 486 if (component != null) 487 component.repaint(); 488 } 489 } 490 491 492 493 } 494 495 496 497 498 } 499 500 501 502 } 503