/**
* ThisTime 2.0 (2008-03-30)
* Copyright 2007 Zach Scrivena
* zachscrivena@gmail.com
* http://thistime.sourceforge.net/
*
* Simple clock and timer program.
*
* TERMS AND CONDITIONS:
* 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 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package thistime;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
/**
* Clock display.
*/
class ClockDisplay
extends JComponent
{
/** unscaled images for digits, colon, AM, and PM (first 10 elements are digits 0-9) */
private static final BufferedImage[] UNSCALED_IMAGES = new BufferedImage[13];
/** array index of the colon image in UNSCALED_IMAGES[] */
private static final int COLON_INDEX = 10;
/** array index of the AM image in UNSCALED_IMAGES[] */
private static final int AM_INDEX = 11;
/** array index of the PM image in UNSCALED_IMAGES[] */
private static final int PM_INDEX = 12;
/** width of an unscaled digit image */
private static int UNSCALED_DIGIT_WIDTH;
/** height of an unscaled digit image */
private static int UNSCALED_DIGIT_HEIGHT;
/** width of the unscaled colon image */
private static int UNSCALED_COLON_WIDTH;
/** width of the unscaled AM/PM image */
private static int UNSCALED_AM_WIDTH;
/** height of the unscaled AM/PM image */
private static int UNSCALED_AM_HEIGHT;
/** scaling ratio for a small digit (should be less than 1) */
private static final double SMALL_DIGIT_RATIO = 0.6;
/** horizontal and vertical padding between digits */
private static final int PADDING = 20;
/** scaled images for digits, colon, AM, and PM (first 10 elements are digits 0-9) */
private final Image[] scaledImages = new Image[13];
/** scaled images for small digits */
private final Image[] smallScaledImages = new Image[10];
/** hash code for the unscaled total width and height corresponding to scaledImages[] and smallScaledImages[] */
private int unscaledSizeHashCode = -1;
/** hash code for the scaled total width and height corresponding to scaledImages[] and smallScaledImages[] */
private int scaledSizeHashCode = -1;
/** clock value to be displayed */
private final ClockValue value;
/**
* Static initialization block for loading image resources.
* The following assumptions are made:
* 1. All digit images have the same dimensions.
* 2. Digits and the colon images have the same height.
* 3. AM/PM images have the same dimensions.
*/
static
{
try
{
for (int i = 0; i <= 9; i++)
{
UNSCALED_IMAGES[i] = ImageIO.read(ClockDisplay.class.getResource("/thistime/resources/digit" + i + ".png"));
}
UNSCALED_IMAGES[COLON_INDEX] = ImageIO.read(ClockDisplay.class.getResource("/thistime/resources/colon.png"));
UNSCALED_IMAGES[AM_INDEX] = ImageIO.read(ClockDisplay.class.getResource("/thistime/resources/am.png"));
UNSCALED_IMAGES[PM_INDEX] = ImageIO.read(ClockDisplay.class.getResource("/thistime/resources/pm.png"));
UNSCALED_DIGIT_WIDTH = UNSCALED_IMAGES[0].getWidth();
UNSCALED_DIGIT_HEIGHT = UNSCALED_IMAGES[0].getHeight();
UNSCALED_COLON_WIDTH = UNSCALED_IMAGES[COLON_INDEX].getWidth();
UNSCALED_AM_WIDTH = UNSCALED_IMAGES[AM_INDEX].getWidth();
UNSCALED_AM_HEIGHT = UNSCALED_IMAGES[AM_INDEX].getHeight();
}
catch (Exception e)
{
throw new RuntimeException(e.getMessage(), e.getCause());
}
}
/**
* Constructor.
*
* @param value
* "normalized" timer value to be displayed
* @param setMode
* is the timer display in "set" mode initially?
*/
ClockDisplay(
final ClockValue value)
{
this.value = value;
}
/**
* Paint the clock display. This method is invoked by Swing to draw components.
*
* @param g
* the Graphics context in which to paint
*/
@Override
public void paint(
Graphics g)
{
synchronized (value)
{
doAction(Action.PAINT, g);
}
}
/**
* Perform the specified action on the display.
* Assumes that value has been externally synchronized.
* This method must run on the EDT.
*
* @param action
* action to be performed
* @param arg
* argument value for the action
*/
private void doAction(
final Action action,
final Object arg)
{
/************************************************
* (1) PARSE AGRUMENTS FOR THE SPECIFIED ACTION *
************************************************/
Graphics g = null;
switch (action)
{
case PAINT:
g = (Graphics) arg;
break;
}
/******************************************************
* (2) COMPUTE DIMENSIONS OF CLOCK DISPLAY COMPONENTS *
******************************************************/
/* length of the hour-value */
final int hourValueLength = (value.hour24) ? 2 : String.valueOf(value.h).length();
/* width and height of the canvas */
final Rectangle bounds = getBounds();
final int canvasWidth = (int) bounds.getWidth();
final int canvasHeight = (int) bounds.getHeight();
/* total width and height of timer display, in unscaled units */
final int unscaledTotalWidth = UNSCALED_COLON_WIDTH +
(int) ((SMALL_DIGIT_RATIO * 2 + hourValueLength + 2) * UNSCALED_DIGIT_WIDTH) +
(6 + hourValueLength) * PADDING;
final int unscaledTotalHeight = UNSCALED_DIGIT_HEIGHT + 2 * PADDING;
/* compute scaled widths and heights */
final Dimension scaledDimension = GraphicsManipulator.scaleToFitKeepRatio(
unscaledTotalWidth, unscaledTotalHeight,
canvasWidth, canvasHeight);
final int scaledTotalWidth = (int) scaledDimension.getWidth();
final int scaledTotalHeight = (int) scaledDimension.getHeight();
final int digitWidth = scaledTotalWidth * UNSCALED_DIGIT_WIDTH / unscaledTotalWidth;
final int digitHeight = scaledTotalHeight * UNSCALED_DIGIT_HEIGHT / unscaledTotalHeight;
final int colonWidth = scaledTotalWidth * UNSCALED_COLON_WIDTH / unscaledTotalWidth;
final int smallDigitWidth = (int) (SMALL_DIGIT_RATIO * digitWidth);
final int smallDigitHeight = (int) (SMALL_DIGIT_RATIO * digitHeight);
final int padding = scaledTotalWidth * PADDING / unscaledTotalWidth;
final Dimension scaledAmDimension = GraphicsManipulator.scaleToFitKeepRatio(
UNSCALED_AM_WIDTH,
UNSCALED_AM_HEIGHT,
2 * smallDigitWidth + padding,
scaledTotalHeight - smallDigitHeight - 3 * padding);
final int amWidth = (int) scaledAmDimension.getWidth();
final int amHeight = (int) scaledAmDimension.getHeight();
/* initial horizontal and vertical offsets */
int offsetX = padding + (canvasWidth - scaledTotalWidth) / 2;
int offsetY = padding + (canvasHeight - scaledTotalHeight) / 2;
/***************************************************
* (3) CHECK IF CACHED SCALED IMAGES CAN BE REUSED *
***************************************************/
/* compute new hash codes */
final int newUnscaledSizeHashCode = unscaledTotalWidth * unscaledTotalHeight;
final int newScaledSizeHashCode = scaledTotalWidth * scaledTotalHeight;
if ((newUnscaledSizeHashCode != unscaledSizeHashCode) ||
(newScaledSizeHashCode != scaledSizeHashCode))
{
/* proceed to generate new scaled images */
for (int i = 0; i <= 9; i++)
{
scaledImages[i] = ((digitWidth == 0) || (digitHeight == 0)) ?
UNSCALED_IMAGES[i] :
UNSCALED_IMAGES[i].getScaledInstance(digitWidth, digitHeight, Image.SCALE_SMOOTH);
smallScaledImages[i] = ((smallDigitWidth == 0) || (smallDigitHeight == 0)) ?
UNSCALED_IMAGES[i] :
UNSCALED_IMAGES[i].getScaledInstance(smallDigitWidth, smallDigitHeight, Image.SCALE_SMOOTH);
}
scaledImages[COLON_INDEX] = ((colonWidth == 0) || (digitHeight == 0)) ?
UNSCALED_IMAGES[COLON_INDEX] :
UNSCALED_IMAGES[COLON_INDEX].getScaledInstance(colonWidth, digitHeight, Image.SCALE_SMOOTH);
scaledImages[AM_INDEX] = ((amWidth == 0) || (amHeight == 0)) ?
UNSCALED_IMAGES[AM_INDEX] :
UNSCALED_IMAGES[AM_INDEX].getScaledInstance(amWidth, amHeight, Image.SCALE_SMOOTH);
scaledImages[PM_INDEX] = ((amWidth == 0) || (amHeight == 0)) ?
UNSCALED_IMAGES[PM_INDEX] :
UNSCALED_IMAGES[PM_INDEX].getScaledInstance(amWidth, amHeight, Image.SCALE_SMOOTH);
/* update hash codes */
unscaledSizeHashCode = newUnscaledSizeHashCode;
scaledSizeHashCode = newScaledSizeHashCode;
}
/***********************
* (4) DISPLAY "HOURS" *
***********************/
/* draw the digits in "hours", starting with the most significant digit */
long hourValueLeft = value.h;
for (int i = (hourValueLength - 1); i >= 0; i--)
{
final int divisor = (int) Math.pow(10, i);
final int digit = (int) (hourValueLeft / divisor);
hourValueLeft -= (digit * divisor);
if (action == Action.PAINT)
{
g.drawImage(scaledImages[digit], offsetX, offsetY, null);
}
offsetX += (digitWidth + padding);
}
/* draw the colon */
if (action == Action.PAINT)
{
g.drawImage(scaledImages[COLON_INDEX], offsetX, offsetY, null);
}
offsetX += (colonWidth + padding);
/*************************
* (5) DISPLAY "MINUTES" *
*************************/
/* draw the digits in "minutes", starting with the most significant digit */
int minuteValueLeft = value.m;
for (int i = 1; i >= 0; i--)
{
final int divisor = (int) Math.pow(10, i);
final int digit = minuteValueLeft / divisor;
minuteValueLeft -= (digit * divisor);
if (action == Action.PAINT)
{
g.drawImage(scaledImages[digit], offsetX, offsetY, null);
}
offsetX += (digitWidth + padding);
}
/*******************************************
* (6) PAINT AM/PM, IF 12-HOUR TIME FORMAT *
*******************************************/
if (!value.hour24)
{
if (action == Action.PAINT)
{
g.drawImage(
scaledImages[(value.am) ? AM_INDEX : PM_INDEX],
offsetX + (2 * smallDigitWidth + padding - amWidth) / 2,
offsetY + (scaledTotalHeight - smallDigitHeight - 3 * padding - amHeight) / 2,
null);
}
}
/*************************
* (7) DISPLAY "SECONDS" *
*************************/
/* draw the digits in "seconds", starting with the most significant digit */
int secondValueLeft = value.s;
for (int i = 1; i >= 0; i--)
{
final int divisor = (int) Math.pow(10, i);
final int digit = secondValueLeft / divisor;
secondValueLeft -= (digit * divisor);
if (action == Action.PAINT)
{
g.drawImage(smallScaledImages[digit], offsetX, offsetY + digitHeight - smallDigitHeight, null);
}
offsetX += (smallDigitWidth + padding);
}
}
/*****************
* INNER CLASSES *
*****************/
/**
* Types of action for the doAction() method.
*/
private static enum Action
{
PAINT
};
}