66import java .awt .FontMetrics ;
77import java .awt .Graphics2D ;
88import java .awt .Rectangle ;
9+ import java .awt .RenderingHints ;
910import java .awt .Stroke ;
1011import java .awt .image .BufferedImage ;
12+ import java .util .ArrayList ;
1113import java .util .List ;
1214
1315import org .janelia .alignment .spec .TileSpec ;
1820 * Simple utility to render bounding boxes for tiles.
1921 * If there is enough area to display them, tile identifiers are also rendered inside each box.
2022 *
23+ * <p>The font size is fixed at {@value #TILE_ID_FONT_SIZE} points regardless of render scale.
24+ * For each tile the number of characters that fit on one line is computed from the actual
25+ * scaled box width and the font metrics, so labels wrap correctly at any scale. If the box
26+ * is too narrow to display even one character the label is omitted entirely for that tile.</p>
27+ *
2128 * @author Eric Trautman
2229 */
2330public class BoundingBoxRenderer {
2431
32+ /** Point size of the font used to render tile identifiers. */
33+ public static final int TILE_ID_FONT_SIZE = 12 ;
34+
35+ /**
36+ * Fraction of each box dimension reserved as padding margin when deciding whether tile-id
37+ * text fits. A value of 1.3 means the text block must fit within ~77% of the box width
38+ * and height (i.e. {@code usable = boxDimension / TILE_ID_BOX_MARGIN}).
39+ */
40+ public static final double TILE_ID_BOX_MARGIN = 1.3 ;
41+
2542 private final RenderParameters renderParameters ;
2643 private final double xOffset ;
2744 private final double yOffset ;
@@ -38,12 +55,10 @@ public BoundingBoxRenderer(final RenderParameters renderParameters,
3855 public BoundingBoxRenderer (final RenderParameters renderParameters ,
3956 final Color foregroundColor ,
4057 final float lineWidth ) {
41-
4258 this .renderParameters = renderParameters ;
4359 this .xOffset = renderParameters .getX ();
4460 this .yOffset = renderParameters .getY ();
4561 this .scale = renderParameters .getScale ();
46-
4762 this .foregroundColor = foregroundColor ;
4863
4964 if (renderParameters .getBackgroundRGBColor () == null ) {
@@ -60,6 +75,11 @@ public void render(final BufferedImage targetImage)
6075
6176 final Graphics2D targetGraphics = targetImage .createGraphics ();
6277
78+ targetGraphics .setRenderingHint (RenderingHints .KEY_TEXT_ANTIALIASING ,
79+ RenderingHints .VALUE_TEXT_ANTIALIAS_ON );
80+ targetGraphics .setRenderingHint (RenderingHints .KEY_RENDERING ,
81+ RenderingHints .VALUE_RENDER_QUALITY );
82+
6383 targetGraphics .setColor (foregroundColor );
6484 targetGraphics .setStroke (stroke );
6585
@@ -69,52 +89,66 @@ public void render(final BufferedImage targetImage)
6989 }
7090
7191 final List <TileSpec > tileSpecs = renderParameters .getTileSpecs ();
72- final int maxCharactersPerLine = 12 ;
7392
74- int lineWidth = 0 ;
93+ FontMetrics metrics = null ;
7594 int lineHeight = 0 ;
76- int minBoxWidthForTileIdRendering = 0 ;
77- if (tileSpecs .size () > 0 ) {
95+ int charWidth = 0 ; // width of a single monospaced character
96+
97+ if (! tileSpecs .isEmpty ()) {
7898 targetGraphics .setFont (TILE_ID_FONT );
79- final FontMetrics metrics = targetGraphics .getFontMetrics ();
80- lineWidth = metrics .stringWidth ("A" ) * maxCharactersPerLine ;
99+ metrics = targetGraphics .getFontMetrics ();
81100 lineHeight = metrics .getHeight ();
82-
83- // add margin that should be good enough for 'typical' overlap
84- minBoxWidthForTileIdRendering = (int ) (lineWidth * 1.3 ) + 1 ;
101+ // MONOSPACED: all characters have the same advance width.
102+ charWidth = metrics .charWidth ('A' );
85103 }
86104
87- Rectangle box ;
88- String tileId ;
89- int x ;
90- int y ;
91- int start ;
92105 for (final TileSpec tileSpec : tileSpecs ) {
93106
94- box = getScaledBox (tileSpec );
107+ final Rectangle box = getScaledBox (tileSpec );
95108 targetGraphics .draw (box );
96109
97- if (box .width > minBoxWidthForTileIdRendering ) {
110+ final String tileId = tileSpec .getTileId ();
111+ if (tileId == null || tileId .isEmpty ()) {
112+ continue ;
113+ }
98114
99- tileId = tileSpec .getTileId ();
115+ // Derive how many margin pixels to leave on each side (same fraction as before).
116+ final int usableWidth = (int ) (box .width / TILE_ID_BOX_MARGIN );
100117
101- if (tileId != null ) {
102- x = box .x + ((box .width - lineWidth ) / 2 ); // center tileId horizontally
103- y = box .y + (box .height / 4 ); // shift tileId down from top to avoid 'typical' overlap
118+ // How many characters fit on one line inside this box?
119+ final int charsPerLine = usableWidth / charWidth ;
104120
105- start = 0 ;
106- for (int stop = maxCharactersPerLine ; stop < tileId .length (); stop += maxCharactersPerLine ) {
107- targetGraphics .drawString (tileId .substring (start , stop ), x , y );
108- y = y + lineHeight ;
109- start = stop ;
110- }
111- if (start < tileId .length ()) {
112- targetGraphics .drawString (tileId .substring (start ), x , y );
113- }
114- }
121+ // If not even one character fits, skip the label for this tile.
122+ if (charsPerLine < 1 ) {
123+ continue ;
124+ }
125+
126+ // How many lines does this tile id need, and do they fit vertically?
127+ final List <String > lines = wrapText (tileId , charsPerLine );
128+ final int totalTextHeight = lines .size () * lineHeight ;
115129
130+ // Only draw if the wrapped text fits inside the usable box height (same margin as width).
131+ final int usableHeight = (int ) (box .height / TILE_ID_BOX_MARGIN );
132+ if (totalTextHeight > usableHeight ) {
133+ continue ;
134+ }
135+
136+ // Measure the widest line so we can centre the block horizontally.
137+ int maxLineWidth = 0 ;
138+ for (final String line : lines ) {
139+ final int w = metrics .stringWidth (line );
140+ if (w > maxLineWidth ) {
141+ maxLineWidth = w ;
142+ }
116143 }
117144
145+ final int x = box .x + ((box .width - maxLineWidth ) / 2 ); // centre horizontally
146+ int y = box .y + ((box .height - totalTextHeight ) / 2 ) + metrics .getAscent (); // centre vertically
147+
148+ for (final String line : lines ) {
149+ targetGraphics .drawString (line , x , y );
150+ y += lineHeight ;
151+ }
118152 }
119153
120154 if (renderParameters .isAddWarpFieldDebugOverlay ()) {
@@ -129,6 +163,25 @@ public void render(final BufferedImage targetImage)
129163 LOG .debug ("render: exit, boxes for {} tiles rendered" , tileSpecs .size ());
130164 }
131165
166+ /**
167+ * Wraps {@code text} into lines of at most {@code maxChars} characters each,
168+ * breaking only at character boundaries (no word-wrap, matching the original behaviour).
169+ *
170+ * @param text the string to wrap.
171+ * @param maxChars maximum number of characters per line (must be >= 1).
172+ * @return list of lines, never empty.
173+ */
174+ static List <String > wrapText (final String text , final int maxChars ) {
175+ final List <String > lines = new ArrayList <>();
176+ int start = 0 ;
177+ while (start < text .length ()) {
178+ final int end = Math .min (start + maxChars , text .length ());
179+ lines .add (text .substring (start , end ));
180+ start = end ;
181+ }
182+ return lines ;
183+ }
184+
132185 private Rectangle getScaledBox (final TileSpec tileSpec ) {
133186 final double x = (tileSpec .getMinX () - xOffset ) * scale ;
134187 final double y = (tileSpec .getMinY () - yOffset ) * scale ;
@@ -139,5 +192,5 @@ private Rectangle getScaledBox(final TileSpec tileSpec) {
139192
140193 private static final Logger LOG = LoggerFactory .getLogger (BoundingBoxRenderer .class );
141194
142- private static final Font TILE_ID_FONT = new Font (Font .MONOSPACED , Font .PLAIN , 12 );
143- }
195+ private static final Font TILE_ID_FONT = new Font (Font .MONOSPACED , Font .BOLD , TILE_ID_FONT_SIZE );
196+ }
0 commit comments