Creating a 16-segment character display component in Swing
Some time ago, Javalobby posted a nice article on how someone created a event talk board that resembled a departure board at the airport or trainstation.
I needed to brush up my Java2D skills, so I started on something easy that was inspired by that article: a 16-segment character display (as described here). It should only use Java2D (no images) and capable of showing A-Z and 0-9.
After an evening of drawing on paper grid, this is the end result:

The code is divided in 3 parts: the single character component, a character group component capable of displaying a String and the alfabet definition.
First I'll show the single character component:
public class SixteenSegmentDisplay extends JComponent {
private java.util.List<Integer> lightedSegments;
public static final int WIDTH = 29;
public static final int HEIGHT = 46;
private final double INACTIVE_COLOR_DARKEN_FRACTION = 0.4;
private static final Image BACKGROUND_IMAGE = createBackgroundImage();
// Highlight Colors
private static final java.awt.Color HIGHLIGHT_COLOR_TOP = new java.awt.Color(0x000000);
private static final java.awt.Color HIGHLIGHT_COLOR_BOTTOM = new java.awt.Color(0x625C52);
private Color activeSegmentColor = new Color(0xFF0000);
private Color inactiveSegmentColor = darken(new Color(0xFF0000), INACTIVE_COLOR_DARKEN_FRACTION);
private Color backgroundColor = Color.black;
public SixteenSegmentDisplay() {
lightedSegments = new ArrayList<Integer>();
}
@Override
public java.awt.Dimension getSize() {
return new Dimension(WIDTH, HEIGHT);
}
@Override
public java.awt.Dimension getPreferredSize() {
return new Dimension(WIDTH, HEIGHT);
}
@Override
public java.awt.Dimension getSize(java.awt.Dimension rv) {
return new Dimension(WIDTH, HEIGHT);
}
public void setSegmentColor(Color segmentColor) {
activeSegmentColor = segmentColor;
inactiveSegmentColor = darken(segmentColor, INACTIVE_COLOR_DARKEN_FRACTION);
}
public void setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
}
public void setCharacter(char character) {
setLightedSegments(SixteenSegmentAlfabet.MAP.get(character));
}
private void setLightedSegments(SixteenSegmentAlfabet character) {
if (character != null && character.getSegments() != null)
this.lightedSegments = Arrays.asList(character.getSegments());
else
this.lightedSegments = new ArrayList<Integer>(0);
this.repaint();
}
private Color darken(Color c, double fragment) {
Double newRed = fragment * c.getRed();
Double newBlue = fragment * c.getBlue();
Double newGreen = fragment * c.getGreen();
return new Color(newRed.intValue(), newGreen.intValue(), newBlue.intValue());
}
@Override
public void paint(Graphics g) {
Graphics2D graphics = (Graphics2D) g.create();
graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setRenderingHint(java.awt.RenderingHints.KEY_ALPHA_INTERPOLATION, java.awt.RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
graphics.setRenderingHint(java.awt.RenderingHints.KEY_COLOR_RENDERING, java.awt.RenderingHints.VALUE_COLOR_RENDER_QUALITY);
paintBackground(graphics);
// left top to top middle
paintSegment(graphics, lightedSegments.contains(1), getPoints("2,2;3,1;14,1;15,2;14,3;3,3;2,2"));
// top middle to right top
paintSegment(graphics, lightedSegments.contains(2), getPoints("15,2;16,1;27,1;28,2;27,3;16,3;15,2"));
// left top to left middle
paintSegment(graphics, lightedSegments.contains(3), getPoints("2,2;3,3;3,22;2,23;1,22;1,3;2,2"));
// top left to middle middle
paintSegment(graphics, lightedSegments.contains(4), getPoints("4,4;5,4;13,19;13,21;12,21;4,6;4,4"));
// top middle to middle middle
paintSegment(graphics, lightedSegments.contains(5), getPoints("15,2;14,3;14,22;15,23;16,22;16,3;15,2"));
// top right to middle middle
paintSegment(graphics, lightedSegments.contains(6), getPoints("25,4;26,4;26,6;18,21;17,21;17,19;25,4"));
// top middle to right middle
paintSegment(graphics, lightedSegments.contains(7), getPoints("28,2;29,3;29,22;28,23;27,22;27,3;28,2"));
// left middle to middle middle
paintSegment(graphics, lightedSegments.contains(8), getPoints("2,23;3,22;14,22;15,23;14,24;3,24;2,23"));
// middle middle to right middle
paintSegment(graphics, lightedSegments.contains(9), getPoints("15,23;16,22;27,22;28,23;27,24;16,24;15,23"));
// left middle to bottom left
paintSegment(graphics, lightedSegments.contains(10), getPoints("2,23;3,24;3,43;2,44;1,43;1,24;2,23"));
// left bottom to middle middle
paintSegment(graphics, lightedSegments.contains(11), getPoints("13,27;13,25;12,25;4,40;4,42;5,42;13,27"));
// bottom middle to middle middle
paintSegment(graphics, lightedSegments.contains(12), getPoints("15,23;16,24;16,43;15,44;14,43;14,24;15,23"));
// bottom right to middle middle
paintSegment(graphics, lightedSegments.contains(13), getPoints("17,25;18,25;26,40;26,42;25,42;17,27;17,25"));
// right middle to bottom right
paintSegment(graphics, lightedSegments.contains(14), getPoints("28,23;29,24;29,43;28,44;27,43;27,24;28,23"));
// bottpm left to bottom middle
paintSegment(graphics, lightedSegments.contains(15), getPoints("2,44;3,43;14,43;15,4;14,45;3,45;2,44"));
// bottom middle to bottom right
paintSegment(graphics, lightedSegments.contains(16), getPoints("15,244;16,43;27,43;28,44;27,45;16,45;15,44"));
}
private void paintBackground(Graphics2D g) {
g.drawImage(BACKGROUND_IMAGE, 0, 0, this);
}
private static final BufferedImage createBackgroundImage() {
GraphicsConfiguration gfxConf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
final BufferedImage IMAGE = gfxConf.createCompatibleImage(WIDTH, HEIGHT, Transparency.OPAQUE);
Graphics2D g2 = IMAGE.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
final java.awt.geom.Point2D START_INNER_BACKGROUND = new java.awt.geom.Point2D.Float(0, 0);
final java.awt.geom.Point2D STOP_INNER_BACKGROUND = new java.awt.geom.Point2D.Float(0, HEIGHT);
final float[] FRACTIONS_INNER_BACKGROUND =
{
0.0f,
1.0f
};
final Color[] COLORS_INNER_BACKGROUND =
{
new Color(0x3D3A31),
new Color(0x232520)
};
final LinearGradientPaint GRADIENT_INNER_BACKGROUND = new LinearGradientPaint(START_INNER_BACKGROUND, STOP_INNER_BACKGROUND, FRACTIONS_INNER_BACKGROUND, COLORS_INNER_BACKGROUND);
g2.setPaint(GRADIENT_INNER_BACKGROUND);
g2.fill(new java.awt.geom.Rectangle2D.Float(0, 0, WIDTH, HEIGHT));
// Highlight
g2.setColor(HIGHLIGHT_COLOR_TOP);
g2.drawLine(0, 0, WIDTH, 0);
g2.setColor(HIGHLIGHT_COLOR_BOTTOM);
g2.drawLine(0, HEIGHT, WIDTH, HEIGHT);
g2.dispose();
return IMAGE;
}
private void paintSegment(Graphics2D g, boolean active, Point... points) {
if (points != null) {
g.setColor(active ? activeSegmentColor : inactiveSegmentColor);
Polygon polygon = new Polygon();
for (Point point : points) {
polygon.addPoint(point.x, point.y);
}
g.fillPolygon(polygon);
}
}
private static Point[] getPoints(String coded) {
if(coded.length() == 0)
{
return null;
}
java.util.List<Point> pointList = new ArrayList<Point>();
String[] points = coded.split(";");
for (String point : points) {
String[] pointCoordinates = point.split(",");
pointList.add(new Point(Integer.parseInt(pointCoordinates[0]), Integer.parseInt(pointCoordinates[1])));
}
return pointList.toArray(new Point[pointList.size()]);
}
}
To use this, you need an alfabet definition that defines which segments should light up for a given character:
public enum SixteenSegmentAlfabet {
A(new char[]{'a', 'A'}, 1, 2, 3, 7, 8, 9, 10, 14),
B(new char[]{'b', 'B'}, 1, 2, 5, 7, 9, 12, 14, 15, 16),
C(new char[]{'c', 'C'}, 1, 2, 3, 10, 15, 16),
D(new char[]{'d', 'D'}, 1, 2, 5, 7, 12, 14, 15, 16),
E(new char[]{'e', 'E'}, 1, 2, 3, 8, 9, 10, 15, 16),
F(new char[]{'f', 'F'}, 1, 2, 3, 8, 10),
G(new char[]{'g', 'G'}, 1, 2, 3, 9, 10, 14, 15, 16),
H(new char[]{'h', 'H'}, 3, 7, 8, 9, 10, 14),
I(new char[]{'i', 'I'}, 1, 2, 5, 12, 15, 16),
J(new char[]{'j', 'J'}, 7, 10, 14, 15, 16),
K(new char[]{'k', 'K'}, 3, 6, 8, 10, 13),
L(new char[]{'l', 'L'}, 3, 10, 15, 16),
M(new char[]{'m', 'M'}, 3, 4, 6, 7, 10, 14),
N(new char[]{'n', 'N'}, 3, 4, 7, 10, 13, 14),
O(new char[]{'o', 'O'}, 1, 2, 3, 7, 10, 14, 15, 16),
P(new char[]{'p', 'P'}, 1, 2, 3, 6, 8, 10),
Q(new char[]{'q', 'Q'}, 1, 2, 3, 7, 10, 13, 14, 15, 16),
R(new char[]{'r', 'R'}, 1, 2, 3, 7, 8, 9, 10, 13),
S(new char[]{'s', 'S'}, 1, 2, 3, 8, 9, 14, 15, 16),
T(new char[]{'t', 'T'}, 1, 2, 5, 12),
U(new char[]{'u', 'U'}, 3, 7, 10, 14, 15, 16),
V(new char[]{'v', 'V'}, 3, 6, 10, 11),
W(new char[]{'w', 'W'}, 3, 7, 10, 11, 13, 14),
X(new char[]{'x', 'X'}, 4, 6, 11, 13),
Y(new char[]{'y', 'Y'}, 4, 6, 12),
Z(new char[]{'z', 'Z'}, 1, 2, 6, 11, 15, 16),
BLANK(new char[]{' '}, null),
ZERO(new char[]{'0'}, 1, 2, 3, 4, 7, 10, 13, 14, 15, 16),
ONE(new char[]{'1'}, 7, 14),
TWO(new char[]{'2'}, 1, 2, 7, 8, 9, 10, 15, 16),
THREE(new char[]{'3'}, 1, 2, 7, 9, 14, 15, 16),
FOUR(new char[]{'4'}, 3, 7, 8, 9, 14),
FIVE(new char[]{'5'}, 1, 2, 3, 8, 13, 15, 16),
SIX(new char[]{'6'}, 1, 2, 3, 8, 9, 10, 14, 15, 16),
SEVEN(new char[]{'7'}, 1, 2, 7, 14),
EIGHT(new char[]{'8'}, 1, 2, 3, 7, 8, 9, 10, 14, 15, 16),
NINE(new char[]{'9'}, 1, 2, 3, 7, 8, 9, 14, 15, 16);
private final Integer[] segments;
private final char[] characters;
public static final Map<Character, SixteenSegmentAlfabet> MAP;
static {
MAP = new HashMap<Character, SixteenSegmentAlfabet>(36);
for (SixteenSegmentAlfabet alfabet : SixteenSegmentAlfabet.values()) {
for (char character : alfabet.characters) {
MAP.put(character, alfabet);
}
}
}
SixteenSegmentAlfabet(char[] characters, Integer... segments) {
this.characters = characters;
this.segments = segments;
}
public Integer[] getSegments() {
return segments;
}
}
To finish, the component capable of showing a String merely groups together a couple of the character components:
public class MultiCharacterDisplay extends JComponent {
private static final int GAP = 5;
private int segmentDisplayCount;
private List<SixteenSegmentDisplay> segmentDisplays;
public MultiCharacterDisplay(int segmentDisplayCount) {
this(segmentDisplayCount, Color.red);
}
public MultiCharacterDisplay(int segmentDisplayCount, Color segmentColor) {
this.segmentDisplayCount = segmentDisplayCount;
segmentDisplays = new ArrayList<SixteenSegmentDisplay>(segmentDisplayCount);
setLayout(new FlowLayout(FlowLayout.LEFT, GAP, 0));
for (int i = 0; i < segmentDisplayCount; i++) {
SixteenSegmentDisplay display = new SixteenSegmentDisplay();
display.setSegmentColor(segmentColor);
segmentDisplays.add(display);
add(display);
}
}
public void setText(String text) {
if (text.length() > segmentDisplayCount) {
text = text.substring(0, segmentDisplayCount);
}
int i = 0;
for (char c : text.toCharArray()) {
segmentDisplays.get(i++).setCharacter(c);
}
}
}
All in all, it was a nice exercise
. If I find some more time, I'll try and build some sort of marquee with it.
Code Kata: BBCode
Here's a nice code kata: convert BBCode to HTML. So [b]test[/b] becomes <strong>test</strong>, etc... Provide tags for all the basic BBCode tags: i, u, b, img, url, li, ol, ul and newlines.
For you cheaters, here's my implementation, using regex:
import org.springframework.web.util.HtmlUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BBCodeConverter {
public static String convertString(String bbCodeString)
{
bbCodeString = HtmlUtils.htmlEscape(bbCodeString);
for(BBCodeTag tag : BBCodeTag.values())
{
bbCodeString = tag.convert(bbCodeString);
}
return bbCodeString;
}
private enum BBCodeTag
{
B("\\[b\\](.*?)\\[/b\\]", "<strong>$1</strong>"),
I("\\[i\\](.*?)\\[/i\\]", "<em>$1</em>"),
U("\\[u\\](.*?)\\[/u\\]", "<span style=\"text-decoration:underline\">$1</span>"),
NEWLINE("\n", "<br/>"),
IMG("\\[img\\](.*?)\\[/img\\]", "<img src=\"$1\"></img>"),
URL("\\[url=(.*?)\\](.*?)\\[/url\\]", "<a href=\"$1\">$2</a>"),
UL("\\[ul\\](.*?)\\[/ul\\]","<ul>$1</ul>"),
OL("\\[ol\\](.*?)\\[/ol\\]","<ol>$1</ol>"),
LI("\\[li\\](.*?)\\[/li\\]","<li>$1</li>"),
SIZE("\\[size=(.*?)\\](.*?)\\[/size\\]","<span style=\"font-size:$1%\">$2</span>"),
COLOR("\\[color=(.*?)\\](.*?)\\[/color\\]","<span style=\"color:$1\">$2</span>")
;
private String tagPattern;
private String htmlConversion;
private BBCodeTag(String tagPattern, String htmlConversion) {
this.htmlConversion = htmlConversion;
this.tagPattern = tagPattern;
}
public String convert(String bbCodeTag)
{
Pattern pattern = Pattern.compile(tagPattern);
Matcher matcher = pattern.matcher(bbCodeTag);
String replaced = matcher.replaceAll(htmlConversion);
return replaced;
}
}
}
Using JIRA efficiently
Let's face it, there is only one issue tracker worth using, and that's JIRA. Believe me, I've used quite a few (Mantis, Savannah, Trac, Bugzilla, FogBugz, ...) and none of the other have the same power and user friendliness like JIRA.
JIRA also has a ton of plugins, a lot of which are free. And even if they aren't, the investment usually repays itself within months. The ROI is incredible. Today, I'll shed some light on the plugins I usually install on any JIRA I get to do administration on.
Tempo
JIRA's worklog system is great. However, once you start to use Tempo, you'll never use the standard 'Log work' again. Tempo is a complete time tracking system, including holiday tracking, non-issue related time tracking, detailed reporting and integration with ERP and CRM systems (although I haven't used that functionality yet). You do need to do some configuration, but once you set it up, it's invaluable. Using Tempo promotes the idea that all work done on a project should be issue related, an idea I support wholeheartedly.
Price: free when using the Jira Starter license ($10 donation is appreciated), $300 for 25 users to $2000 for unlimited users. So if you can invoice 1/2 hour per person a month more because of accurate time tracking and you're invoicing 25 people at $50/h, you have a ROI of more than 200% in just one month.
Greenhopper
An official Atlassian plugin. If you're using Scrum or any other Agile system and you're using JIRA, you really need this plugin. It's invaluable when doing planning and following up sprints. Even if you're not going agile, this plugin can really help keeping the people in the project informed.
Price: $10 for 10 users (Starter license), $600 for 25 users to $4000 for unlimited users. Usually repays itself through increased team productivity and follow-up in a couple of months.
FishEye plugin
As far as online source control browser tools go, FishEye is a good as it gets. Atlassian provides great integration (off course, FishEye is one of their products). Can be replaced with the specific source control integration plugins, but if you can afford FishEye, use it.
Price: free (FishEye not included)
JIRA Labels
Enabled users to easily add metadata tags to issues, facilitating searches.
Price: free
Compiling JasperReports with Maven
For those working with JasperReports, this is the plugin config you can use to compile jrxml files:
...
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>target/generated-jasper</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jasperreports-maven-plugin</artifactId>
<version>1.0-beta-2</version>
<executions>
<execution>
<goals>
<goal>compile-reports</goal>
</goals>
</execution>
</executions>
<configuration>
<outputDirectory>${project.build.directory}/generated-jasper</outputDirectory>
</configuration>
<dependencies>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>3.7.2</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
...
If you're using groovy as a template language in your JasperReport reports, don't forget to add the groovy-all 1.5.5 dependency to your plugin and add this to your plugin configuration:
<compiler>net.sf.jasperreports.compilers.JRGroovyCompiler</compiler>
Have fun reporting!