Looking for advice on performance and code structure - Thermometer class

I've developed the following Thermometer.class.

This is my first class intended for reuse, so I'd appreciate tips on how to improve it, simplify it, etc

One of the problems I had was to decide if I should extend some existing class or not. I ended up extending JPanel mainly because I wanted to be able to lay on it labels and panels. But this lead me to a Thermometer class having all those inherited methods that I don't need and don't want to bother with. In fact, I only take advantage of a few, related with position and size, such as: setSize(), getPreferredSize(),getBounds(), getInsets().

...So extending JPanel was probably not the best option. Could I do otherwise and still lay my labels and panels?

package ul.cftc.physics.common.graph;

import java.awt.Color;

import java.awt.geom.*;

import java.awt.Graphics;

import java.awt.Graphics2D;

import java.awt.Dimension;

import java.awt.Shape;

import java.awt.event.MouseListener;

import java.awt.event.MouseEvent;

import javax.swing.BorderFactory;

import javax.swing.JLabel;

import javax.swing.JPanel;

import ul.cftc.physics.common.Side;

publicclass Thermometerextends JPanelimplements MouseListener{

/*-- Constructor vars --*/

// temperature

private String unit;// temperature unit used in all the settings; will show in the current temp display

privatedouble minTemp, maxTemp;// min and max temp values; will be labeled

privatedouble minEnabledTemp;// min temperature that is possible to set

privatedouble temp;//current temperature

privatedouble oneUnitHeight;// height corresponding to 1 unit of temperature

// scale

privateint unitsBetweenLargeStrokes;// temperature range between small/large scale strokes

privateint largeToSmallStrokeRangeFactor;//factor between unitsBetweenLargeStrokes and unitsBetweenSmallStrokes

privateint labeledTempToLargeStrokeRangeFactor;//factor between unitsBetweenLabeledTemp and unitsBetweenLargeStrokes

privateboolean scaleOnRightSide;//on which side of the column will scale be drawn

// drawing sizes

privateint insetWidth;//border width (space left all around without drawing)

privateint columnWidth, columnHeight;// width and height of "mercury" column

privateint enclosureThickness;// width of thermometer enclosure around "mercury" column

privateint bulbDiameter;// bulb diameter

privateint smallStrokeSize, largeStrokeSize;// width of small/large scale strokes

privateint labeledTempGapFromStrokes;// distance between scale strokes and labeled temperatures

privateint labeledTempWidth;// width available for labeling temperatures

//drawing colours

private Color fluidColor;//"mercury" colour

/*- Drawing auxiliary vars --*/

// all x,y values are relative to newOrigin (set at thermometer bulb centre, y axis upward)

privatedouble newOriginX, newOriginY;// coordinates of newOrigin

// mercury column drawing aux

privatedouble columnRadius;//columnWidth/2

privatedouble columnUsefulHeight;//height available for displaying the temp range from minTemp to maxTemp

privatedouble columnUnusedHeight;//columnHeight - columnUsefulHeight

// scale strokes drawing aux

privateint totalNumLargeStrokes;

privatedouble heightBetweenSmallStrokes, heightBetweenLargeStrokes;

privatedouble strokesX;

double largeStrokesEndX;

double smallStrokesEndX;

double labeledTempX;

// thermometer bulb drawing aux

privatedouble bulbRadius;

privatedouble clippedBulbHalfHeight;

privatedouble bulbStartAngleRelativeToNorth;// rad

privatedouble bulbSolidAngle;// rad

// bulb enclosure drawing aux

privatedouble bulbEnclosureRadius;

privatedouble bulbEnclosureStartAngleRelativeToNorth;// rad

privatedouble dblBulbEnclosureSolidAngle;// rad

//MouseListener shape

private Shape shpTempChangeEnabledArea;

//Components

private JLabel lblCurrentTemp;

private JPanel fluid;

/* Default constructor -*/

public Thermometer(String unit,double minTemp,double maxTemp,

int unitsBetweenLargeStrokes,int largeToSmallStrokeRangeFactor,

int labeledTempToLargeStrokeRangeFactor,int bulbDiameter,

int enclosureThickness,int columnWidth,int columnHeight,

Side scaleSide, Color fluidColor){

// initialize required vars with default values

this.unit = unit;

this.minTemp = minTemp;

this.maxTemp = maxTemp;

this.minEnabledTemp = minTemp;

this.unitsBetweenLargeStrokes = unitsBetweenLargeStrokes;

this.largeToSmallStrokeRangeFactor = largeToSmallStrokeRangeFactor;

this.labeledTempToLargeStrokeRangeFactor = labeledTempToLargeStrokeRangeFactor;

this.bulbDiameter = bulbDiameter;

this.enclosureThickness = enclosureThickness;

this.columnWidth = columnWidth;

this.columnHeight = columnHeight;

this.largeStrokeSize = 5;

this.smallStrokeSize = 2;

this.labeledTempGapFromStrokes = 2;

this.labeledTempWidth = 30;

this.fluidColor = fluidColor;

this.insetWidth = 20;

this.temp = maxTemp;

if (scaleSide == Side.RIGHT_SIDE){

this.scaleOnRightSide =true;

}else{

this.scaleOnRightSide =false;

}

// calculate mercury column variables values

columnRadius = columnWidth / 2;

columnUnusedHeight = columnRadius;

columnUsefulHeight = columnHeight - columnUnusedHeight;

oneUnitHeight = columnUsefulHeight / (maxTemp - minTemp);

// calculate thermometer bulb variables values

bulbRadius = bulbDiameter / 2;

bulbStartAngleRelativeToNorth = Math.asin(columnRadius

/ bulbRadius);

bulbSolidAngle = 2 * Math.PI - 2 * bulbStartAngleRelativeToNorth;

clippedBulbHalfHeight = bulbRadius

* Math.cos(bulbStartAngleRelativeToNorth);

// calculate bulb enclosure variables values

bulbEnclosureRadius = bulbRadius + enclosureThickness;

bulbEnclosureStartAngleRelativeToNorth = Math

.asin((columnRadius + enclosureThickness)

/ bulbEnclosureRadius);

dblBulbEnclosureSolidAngle = 2 * Math.PI - 2

* bulbEnclosureStartAngleRelativeToNorth;

this.setLayout(null);

this.setOpaque(true);

this.setBorder(BorderFactory.createEmptyBorder(insetWidth,insetWidth, insetWidth, insetWidth));

this.setBackground(Color.LIGHT_GRAY);

// calculate thermometer size (without insets from border)

double widthInternal, heightInternal;

widthInternal = Math.max(bulbDiameter + 2 * enclosureThickness,

bulbRadius + enclosureThickness + columnRadius

+ enclosureThickness + largeStrokeSize

+ labeledTempGapFromStrokes + labeledTempWidth);

heightInternal= columnHeight + clippedBulbHalfHeight + bulbRadius

+ 2 * enclosureThickness;

// component Dimensions

double width, height;

width = widthInternal + this.getInsets().left + this.getInsets().right;

height = heightInternal + this.getInsets().top

+ this.getInsets().bottom;

Dimension dim =new Dimension((int) Math.round(width), (int) Math.round(height));

this.setPreferredSize( dim);

// set new Origin at thermometer bulb centre, y axis upward

setNewOrigin();

setStrokesPositions();

// add Shape for hit test

setShapeForHitTest() ;

this.addMouseListener(this);

// add Label for displaying current temp positioned at bulb centre

lblCurrentTemp =new JLabel("");

lblCurrentTemp.setBounds(getX(-bulbRadius - 20),

getY(-clippedBulbHalfHeight + 10), bulbDiameter + 40, 20);

lblCurrentTemp.setHorizontalAlignment(JLabel.CENTER);

lblCurrentTemp.setOpaque(false);

lblCurrentTemp.setForeground(Color.BLACK);

this.add(lblCurrentTemp);

// add fluid panel

fluid =new JPanel();

fluid.setOpaque(true);

fluid.setBackground(fluidColor);

// this.add(fluid,new Integer(1));

this.add(fluid);

setTemperature(temp);

// calculate scale strokes variables values

totalNumLargeStrokes = (int) Math.floor((maxTemp - minTemp)

/ unitsBetweenLargeStrokes);// does not count minTemp stroke

heightBetweenLargeStrokes = columnUsefulHeight

/ totalNumLargeStrokes;

heightBetweenSmallStrokes = heightBetweenLargeStrokes

/ largeToSmallStrokeRangeFactor;

}

public Thermometer(){

this("K", 0.0, 8000.0,

500, 5, 2,

50,3, 20, 300,

Side.RIGHT_SIDE ,Color.RED );

}

/* Methods -*/

publicvoid setScale(Side scaleSide){

scaleSide.checkNull(scaleSide);

if (scaleSide==Side.RIGHT_SIDE ){

scaleOnRightSide =true;

}else{

scaleOnRightSide =false;

}

setNewOrigin();

lblCurrentTemp.setBounds(getX(-bulbRadius - 20),

getY(-clippedBulbHalfHeight + 10), bulbDiameter + 40, 20);

setStrokesPositions();

setShapeForHitTest();

resizeFluid();

repaint();

}

privatevoid setStrokesPositions(){

strokesX = columnRadius + enclosureThickness;

if (scaleOnRightSide){

smallStrokesEndX = strokesX + smallStrokeSize;

largeStrokesEndX = strokesX + largeStrokeSize;

labeledTempX = largeStrokesEndX + labeledTempGapFromStrokes;

}else{

strokesX = -strokesX;

smallStrokesEndX = strokesX - smallStrokeSize;

largeStrokesEndX = strokesX - largeStrokeSize;

labeledTempX = largeStrokesEndX - labeledTempGapFromStrokes

- labeledTempWidth;

}

}

privatevoid setShapeForHitTest(){

int shpLeft=(scaleOnRightSide?getX(-columnRadius): getX(largeStrokesEndX));

shpTempChangeEnabledArea =new Rectangle2D.Double(shpLeft, getY(columnHeight),

columnWidth + enclosureThickness + largeStrokeSize,

columnHeight);

}

privatevoid setNewOrigin(){

// set origin at thermometer bulb centre, y axis upward

newOriginX=(scaleOnRightSide?

(enclosureThickness + bulbRadius + this.getInsets().left):

( this.getInsets().left + Math.max(enclosureThickness + bulbRadius, columnRadius

+ enclosureThickness + largeStrokeSize + labeledTempGapFromStrokes + labeledTempWidth)));

newOriginY =columnHeight + enclosureThickness + this.getInsets().top;

}

privateint getX(double X){

return (int) Math.round(newOriginX + X);

}

privateint getY(double Y){

return (int) Math.round(newOriginY - Y);

}

publicdouble getTemperature(){

return temp;

}

publicvoid setTemperature(double t){

// sets temp, "fills" thermometer with fluid and writes temp current value

if (t <=maxTemp && t >= minEnabledTemp ){

temp = t;

resizeFluid();

lblCurrentTemp.setText(Math.round(temp ) +" " + unit);

}

}

publicvoid resizeFluid(){

fluid.setSize(columnWidth - 1, (int) Math.round(temp * oneUnitHeight));

fluid.setLocation(getX(-columnRadius + 1), getY(fluid.getHeight()));

}

publicdouble getMaxTemp(){

return maxTemp;

}

publicdouble getMinTemp(){

return minTemp;

}

publicdouble getMinEnabledTemp(){

return minEnabledTemp;

}

publicvoid setMinEnabledTemp(double t){

minEnabledTemp=t;

}

publicvoid mousePressed(MouseEvent e){

}

publicvoid mouseClicked(MouseEvent e){

if (shpTempChangeEnabledArea.contains( e.getX(), e.getY())){

temp = Math.max(Math.min((newOriginY-e.getY())/oneUnitHeight, maxTemp), minEnabledTemp);

setTemperature(temp );

}

}

publicvoid mouseEntered(MouseEvent e){

}

publicvoid mouseExited(MouseEvent e){

}

publicvoid mouseReleased(MouseEvent e){

}

protectedvoid paintComponent(Graphics g){

// garantee background of component is painted, if opaque

super.paintComponent(g);

// draw static parts of thermometer, i.e., everything except fluid and

// temp current value

Graphics2D g2d = (Graphics2D) g.create();

g2d.setPaint(Color.BLACK);

// g2d.setStroke(new BasicStroke(2));

// draw column

g2d.draw(new Line2D.Double(getX(-columnRadius),

getY(columnUsefulHeight), getX(-columnRadius), getY(0)));

g2d.draw(new Line2D.Double(getX(columnRadius),

getY(columnUsefulHeight), getX(columnRadius), getY(0)));

// draw arc on top of column

g2d.draw(new Arc2D.Double(getX(-columnRadius), getY(columnHeight),

columnWidth, 2 * columnUnusedHeight, 0, 180, Arc2D.OPEN));

// draw bulb

g2d.setPaint(fluidColor);

g2d.fill(new Arc2D.Double(getX(-bulbRadius), getY(bulbRadius

- clippedBulbHalfHeight), bulbDiameter, bulbDiameter, -270

+ bulbStartAngleRelativeToNorth * 180 / Math.PI, bulbSolidAngle

* 180 / Math.PI, Arc2D.OPEN));

g2d.setPaint(Color.BLACK);

g2d.draw(new Arc2D.Double(getX(-bulbRadius), getY(bulbRadius

- clippedBulbHalfHeight), bulbDiameter, bulbDiameter, -270

+ bulbStartAngleRelativeToNorth * 180 / Math.PI, bulbSolidAngle

* 180 / Math.PI, Arc2D.OPEN));

// draw column enclosure

g2d.draw(new Line2D.Double(getX(-columnRadius - enclosureThickness),

getY(columnUsefulHeight), getX(-columnRadius

- enclosureThickness), getY(0) - 1));

g2d.draw(new Line2D.Double(getX(columnRadius + enclosureThickness),

getY(columnUsefulHeight), getX(columnRadius

+ enclosureThickness), getY(0) - 1));

// draw enclosure arc on top of column

g2d.draw(new Arc2D.Double(getX(-columnRadius - enclosureThickness),

getY(columnHeight + enclosureThickness), columnWidth + 2

* enclosureThickness,

2 * (columnUnusedHeight + enclosureThickness), 0, 180,

Arc2D.OPEN));

// draw bulb enclosure

g2d.draw(new Arc2D.Double(getX(-bulbEnclosureRadius), getY(bulbRadius

- clippedBulbHalfHeight + enclosureThickness),

bulbEnclosureRadius * 2, bulbEnclosureRadius * 2, -270

+ bulbEnclosureStartAngleRelativeToNorth * 180

/ Math.PI, dblBulbEnclosureSolidAngle * 180 / Math.PI,

Arc2D.OPEN));

// draw scale strokes and temperature Labels

double h = 0.0;

double nextLargeStrokeHeight;

int n = 0;

StringBuffer sb =new StringBuffer();

StringBuffer sbMax =new StringBuffer();

StringBuffer sbSpaces =new StringBuffer();

int labeledTempStringLength = sbMax.append(maxTemp).length();

for (int i = 1; i < labeledTempStringLength; i++){

sbSpaces.append(" ");

}

int countLargeStrokes=0 ;

int countSmallStrokes=0;

// large strokes

while (countLargeStrokes<=totalNumLargeStrokes){

g2d.drawLine(getX(strokesX), getY(h), getX(largeStrokesEndX),getY(h));

n++;

if (n==labeledTempToLargeStrokeRangeFactor || h == 0){

sb.append((countLargeStrokes * unitsBetweenLargeStrokes));

if (sb.length() < labeledTempStringLength){

sb.insert(0, sbSpaces, 1, labeledTempStringLength

- sb.length());

}

g2d.drawString(sb.toString(), getX(labeledTempX), getY(h) + 3);

sb.delete(1, sb.length());

n=0;

}

// h=h+heightBetweenLargeStrokes;

countLargeStrokes++;

if (countLargeStrokes>totalNumLargeStrokes )break;

nextLargeStrokeHeight=h+heightBetweenLargeStrokes;

//small strokes

countSmallStrokes=1;

h = h + heightBetweenSmallStrokes;

while (countSmallStrokes<largeToSmallStrokeRangeFactor){

g2d.drawLine(getX(strokesX), getY(h), getX(smallStrokesEndX),

getY(h));

h = h + heightBetweenSmallStrokes;

countSmallStrokes++;

}

h = nextLargeStrokeHeight;

}

g2d.dispose();

}

}

Error handling code is still missing (I have to study the exceptions chapter yet...). I'll add that later.

I'd appreciate some advice/comments.

Thank you.>

[26900 byte] By [Joaquinzinhaa] at [2007-10-2 7:57:49]
# 1
no-one will know how to reuse it without javadoc.
Ken_Sa at 2007-7-16 21:48:29 > top of Java-index,Desktop,Core GUI APIs...
# 2

> no-one will know how to reuse it without javadoc.

Ok, that's a thing to keep in mind. I'll wory about that later on though. I'm just starting with Java and before worrying about others using my code I feel that I need to worry if the quality of my code sucks or not.

Anything else?

Joaquinzinhaa at 2007-7-16 21:48:29 > top of Java-index,Desktop,Core GUI APIs...
# 3
You should probably make your thermometer a JComponent (extend JComponent) and then when you use it place it on a JPanel.Making it a JComponent will also make it much easier for reuse by other people since they can then use it however they see fit in their apps.
mjparmea at 2007-7-16 21:48:29 > top of Java-index,Desktop,Core GUI APIs...
# 4

1)

all of this

...implements MouseListener

this.addMouseListener(this);

public void mousePressed(MouseEvent e){

}

public void mouseClicked(MouseEvent e){

if (shpTempChangeEnabledArea.contains( e.getX(), e.getY())){

temp = Math.max(Math.min((newOriginY-e.getY())/oneUnitHeight, maxTemp), minEnabledTemp);

setTemperature(temp );

}

}

public void mouseEntered(MouseEvent e){

}

public void mouseExited(MouseEvent e){

}

public void mouseReleased(MouseEvent e){

}

can be replaced by this

this.addMouseListener(new MouseAdapter(){

public void mouseClicked(MouseEvent e){

if (shpTempChangeEnabledArea.contains( e.getX(), e.getY())){

temp = Math.max(Math.min((newOriginY-e.getY())/oneUnitHeight, maxTemp), minEnabledTemp);

setTemperature(temp );

}});

2)

this.setLayout(null);

will cause you grief later on.

When you have your program to the point you are happy with it, create a copy

of the program and experiment with different layout managers - what you learn

you can carry forward into other gui's

Michael_Dunna at 2007-7-16 21:48:29 > top of Java-index,Desktop,Core GUI APIs...
# 5

> 1)

Thanks. You're right: using an Adapter is definitly simpler in this case.

> 2)

> this.setLayout(null);

> will cause you grief later on.

>

I am not very familiar with all the LayoutManagers capabilities, but the impression I have from LayoutManagers is that they make your life easier when you want components aligned, but not when you want to draw at a precise position. Is this correct?

Could a LayoutManager make the drawing of a set graph scale strokes more automatic, for instance? And position a label exactly at a shape center?

Would you use a Layout Manager to create a XYPlot (that will be next challenge...)?

So many questions....!

Thank you.

Joaquinzinhaa at 2007-7-16 21:48:29 > top of Java-index,Desktop,Core GUI APIs...
# 6

> You should probably make your thermometer a

> JComponent (extend JComponent) and then when you use

> it place it on a JPanel.

>

> Making it a JComponent will also make it much easier

> for reuse by other people since they can then use it

> however they see fit in their apps.

Thanks. I am trying that: i've changed the Thermometer class from "extends JPanel" to "extends JComponent" but now I get a messy painting each time I click to change the temperature.

Ooops, I have to run, I'll have to look at that tomorrow. It seems that the coordinates are no longer the same. Any reason for this when changing from a JPanel to a JComponent?

Here's my testing class:

import ul.cftc.physics.common.Side;

import ul.cftc.physics.common.graph.Thermometer;

import java.awt.Color;

public class TestThermometer extends javax.swing.JApplet{

Thermometer th ;

public void init(){

try{

javax.swing.SwingUtilities.invokeAndWait( new Runnable(){

public void run(){

createGUI();

}

});

}catch (Exception e ) {

System.err.println("createGUI did not complete successfully");

}

}

public void createGUI(){

th = new Thermometer();

th.setBackground( Color.YELLOW );

//this.setContentPane( th);

th.setTemperature( th.getMaxTemp() );

th.setScale(Side.LEFT_SIDE );

//this.setSize(this.getPreferredSize() );

this.setSize(th.getPreferredSize() );

this.add( th);

}

}

Joaquinzinhaa at 2007-7-16 21:48:29 > top of Java-index,Desktop,Core GUI APIs...
# 7

> but the impression I have from LayoutManagers is that they make your life easier when you want components aligned

run your program and see what happens if you drag the frame smaller or bigger

if you are writing the program just for yourself, null layout will probably work fine,

because all the settings are set by you. But if anyone else is going to use it -

different screen sizes, different resolutions etc can make your program a visual nightmare

Here's some links that might start you off

http://java.sun.com/docs/books/tutorial/uiswing/layout/index.html

http://java.sun.com/docs/books/tutorial/uiswing/layout/visual.html

http://java.sun.com/developer/onlineTraining/GUI/AWTLayoutMgr/shortcourse.html

http://mindprod.com/jgloss/layout.html

Michael_Dunna at 2007-7-16 21:48:29 > top of Java-index,Desktop,Core GUI APIs...