001/******************************************************************************* 002 * This software is provided as a supplement to the authors' textbooks on digital 003 * image processing published by Springer-Verlag in various languages and editions. 004 * Permission to use and distribute this software is granted under the BSD 2-Clause 005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit https://imagingbook.com for additional details. 008 ******************************************************************************/ 009package imagingbook.common.ij.overlay; 010 011import ij.gui.Overlay; 012import ij.gui.Roi; 013import ij.gui.ShapeRoi; 014import ij.gui.TextRoi; 015 016import java.awt.BasicStroke; 017import java.awt.Color; 018import java.awt.Font; 019import java.awt.Shape; 020import java.awt.geom.AffineTransform; 021 022/** 023 * <p> 024 * This is an adapter for ImageJ's {@link Overlay} class to ease the insertion of AWT {@link Shape} elements. Shapes are 025 * only geometric descriptions but have no stroke width, color etc. attached. These are determined by 026 * {@link ShapeOverlayAdapter}'s <strong>current state</strong> when insertion is done with {@link #addShape(Shape)} 027 * (i.e., without explicit stroke information). Alternatively, the shape's stroke and color may be explicitly specified 028 * by calling {@link #addShape(Shape, ColoredStroke)}. When a shape is inserted into the associated overlay it is 029 * converted to an ImageJ {@link ShapeRoi} instance whose stroke/fill properties are set. 030 * </p> 031 * <p> 032 * Similarly, text elements can be added using {@link #addText(double, double, String)} or 033 * {@link #addText(double, double, String, Font, Color)}. 034 * </p> 035 * <p> 036 * By default, shapes added to {@link ShapeOverlayAdapter} are automatically translated by 0.5 units in x/y direction, 037 * such that integer coordinates are shifted to pixel centers. This can be deactivated using constructor 038 * {@link #ShapeOverlayAdapter(Overlay, boolean, boolean)}. 039 * </p> 040 * <p> 041 * <strong>Usage example 1 (adding shapes using stroke/color state):</strong> 042 * </p> 043 * <pre> 044 * ImagePlus im = ... 045 * ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 046 * ola.setStroke(new ColoredStroke(0.25, Color.blue)); 047 * ola.addShape(new Line2D.Double(x1, y1, x2, y2)); 048 * ... // add more shapes with same stroke 049 * ola.setStroke(new ColoredStroke(0.4, Color.pink)); 050 * ola.addShape(new Line2D.Double(x3, y3, x4, y4)); 051 * ... // add more shapes with same stroke 052 * im.setOverlay(ola.getOverlay()); 053 * im.show(); 054 * </pre> 055 * <p> 056 * <strong>Usage example 2 (adding shapes with element-wise stroke/color specification), mixed use of {@link Overlay} 057 * and {@link ShapeOverlayAdapter}:</strong> 058 * </p> 059 * <pre> 060 * ImagePlus im = ... 061 * Overlay oly = new Overlay(); 062 * ShapeOverlayAdapter ola = new ShapeOverlayAdapter(oly); 063 * ColoredStroke mystroke = new ColoredStroke(0.25, Color.blue); 064 * ola.addShape(new Line2D.Double(x1, y1, x2, y2), mystroke); 065 * ... // add more shapes 066 * im.setOverlay(oly); 067 * im.show(); 068 * </pre> 069 * <p> 070 * <strong>Usage example 3 (adding text):</strong> 071 * </p> 072 * <pre> 073 * ImagePlus im = ... 074 * ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 075 * ola.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 8)); 076 * ola.setTextColor(Color.green); 077 * ola.addText(x, y, "text using current font/color state"); 078 * ola.addText(x, y, "text with explicit font and color", new Font(Font.SERIF, Font.PLAIN, 10), Color.black); 079 * ... 080 * im.setOverlay(ola.getOverlay()); 081 * im.show(); 082 * </pre> 083 * 084 * @author WB 085 * @version 2022/06/09 086 * @see ColoredStroke 087 * @see Shape 088 */ 089public class ShapeOverlayAdapter { 090 091 public static Color DefaultTextColor = Color.black; 092 public static float DefaultStrokeWidth = 0.5f; 093 public static Font DefaultFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10); 094 095 private static final AffineTransform PixelOffsetTransform = new AffineTransform(1, 0, 0, 1, 0.5, 0.5); // AWT transformation! 096 097 private final boolean antiAlias; 098 private final boolean halfPixelOffset; 099 private final Overlay overlay; 100 101 private ColoredStroke stroke; 102 private Color textColor = DefaultTextColor; 103 private Font font = DefaultFont; 104 private int stackPosition = 0; 105 106 107 // ---------------------------------------------------------- 108 109 public ShapeOverlayAdapter(Overlay oly, boolean halfPixelOffset, boolean antiAlias) { 110 this.overlay = oly; 111 this.halfPixelOffset = halfPixelOffset; 112 this.antiAlias = antiAlias; 113 this.stroke = makeDefaultStroke(); 114 } 115 116 public ShapeOverlayAdapter(Overlay oly) { 117 this(oly, true, true); 118 } 119 120 public ShapeOverlayAdapter() { 121 this(new Overlay()); 122 } 123 124 // ---------------------------------------------------------- 125 126 public ColoredStroke getStroke() { 127 return stroke; 128 } 129 130 public void setStroke(ColoredStroke stroke) { 131 this.stroke = stroke; 132 } 133 134 public void setTextColor(Color color) { 135 this.textColor = color; 136 } 137 138 public void setFont(Font font) { 139 this.font = font; 140 } 141 142 public Overlay getOverlay() { 143 return this.overlay; 144 } 145 146 // ---------------------------------------------------------- 147 148 protected ShapeRoi shapeToRoi(Shape s, ColoredStroke stroke) { 149 s = (halfPixelOffset) ? PixelOffsetTransform.createTransformedShape(s) : s; 150 ShapeRoi roi = new ShapeRoi(s); 151 BasicStroke bs = stroke.getBasicStroke(); 152 roi.setStrokeWidth(bs.getLineWidth()); 153 roi.setStrokeColor(stroke.getStrokeColor()); 154 roi.setFillColor(stroke.getFillColor()); 155 roi.setStroke(bs); 156 roi.setAntiAlias(antiAlias); 157 if (stackPosition > 0) { 158 roi.setPosition(stackPosition); 159 } 160 return roi; 161 } 162 163 public void addShape(Shape s) { 164 overlay.add(shapeToRoi(s, this.stroke)); 165 } 166 167 public void addShapes(Shape[] shapes) { 168 for (Shape s : shapes) { 169 overlay.add(shapeToRoi(s, this.stroke)); 170 } 171 } 172 173 public void addShape(Shape s, ColoredStroke stroke) { 174 overlay.add(shapeToRoi(s, stroke)); 175 } 176 177 public void addShapes(Shape[] shapes, ColoredStroke stroke) { 178 for (Shape s : shapes) { 179 overlay.add(shapeToRoi(s, stroke)); 180 } 181 } 182 183 // TODO: check font/color settings 184 public void addText(double x, double y, String text) { 185 TextRoi troi = new TextRoi(x, y, text, this.font); 186 troi.setStrokeColor(this.textColor); 187 overlay.add(troi); 188 } 189 190 public void addText(double x, double y, String text, Font font, Color textColor) { 191 TextRoi troi = new TextRoi(x, y, text, font); 192 troi.setStrokeColor(textColor); 193 overlay.add(troi); 194 } 195 196 /** 197 * Set the stack position (slice number) where subsequently added shapes should be inserted. See ImageJ's 198 * {@link Roi#setPosition(int)}. 199 * 200 * @param position the stack slice number or zero if no stack insertion is intended 201 */ 202 public void setStackPosition(int position) { 203 stackPosition = position; 204 } 205 206 // ---------------------------------------------------------- 207 208 private static ColoredStroke makeDefaultStroke() { 209 ColoredStroke stroke = new ColoredStroke(); 210 stroke.setLineWidth(DefaultStrokeWidth); 211 stroke.setStrokeColor(DefaultTextColor); 212 return stroke; 213 } 214 215}