// This file is part of PleoCommand: // Interactively control Pleo with psychobiological parameters // // Copyright (C) 2010 Oliver Hoffmann - Hoffmann_Oliver@gmx.de // // 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 2 // 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, write to the Free Software // Foundation, Inc., 51 Franklin Street, Boston, USA. package pleocmd.itfc.gui; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.UIManager; import pleocmd.ImmutableRectangle; import pleocmd.itfc.gui.icons.IconLoader; import pleocmd.pipe.Pipe; import pleocmd.pipe.PipePart; import pleocmd.pipe.cvt.Converter; import pleocmd.pipe.in.Input; import pleocmd.pipe.out.Output; final class BoardPainter { /** * Configuration icon. */ static final Icon ICON_CONF = IconLoader.getIcon("configure"); /** * Visualization icon. */ static final Icon ICON_DGR = IconLoader.getIcon("games-difficult"); /** * The Stroke used to print one Pipe-Flow symbol */ public static final BasicStroke FLOW_STROKE = new BasicStroke( BoardConfiguration.CFG_FLOW_WIDTH.getContent().floatValue(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER, 1); // Miscellaneous private final Set<PipePart> set; private final Map<PipePart, String> saneConfigCache; private final Dimension bounds = new Dimension(); private int border1; private int border2; private double scale = 1.0; private int grayVal = 128; private Pipe pipe; public BoardPainter() { set = new HashSet<PipePart>(); saneConfigCache = new HashMap<PipePart, String>(); } // CS_IGNORE_BEGIN class meant as struct public static class PaintParameters { public Graphics g; public PipePart currentPart; public PipePart underCursor; public Rectangle currentConnection; public PipePart currentConnectionsTarget; public boolean currentConnectionValid; public Icon currentIcon; public BoardAutoLayouter layouter; public boolean modifyable; public Collection<PipeFlow> pipeflow; } // CS_IGNORE_END public void paint(final PaintParameters p) { if (pipe == null || scale <= Double.MIN_NORMAL) return; final Rectangle clip = p.g.getClipBounds(); if (clip == null) return; final BufferedImage img = new BufferedImage(clip.width, clip.height, BufferedImage.TYPE_INT_RGB); final Graphics2D g2 = img.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g2.setClip(0, 0, clip.width, clip.height); final Rectangle clipOrg = new Rectangle((int) (clip.x / scale), (int) (clip.y / scale), (int) (clip.width / scale), (int) (clip.height / scale)); final long start = System.currentTimeMillis(); if (BoardConfiguration.CFG_PAINT_DEBUG.getContent()) { grayVal = (grayVal - 118) % 64 + 128; g2.setColor(new Color(grayVal, grayVal, grayVal)); } else g2.setColor(BoardConfiguration.CFG_BACKGROUND.getContent()); g2.fillRect(0, 0, clip.width, clip.height); g2.translate(-clip.x, -clip.y); g2.scale(scale, scale); drawMovementHint(g2, p); final long time1 = System.currentTimeMillis(); if (clip.x < border1) drawOrderingHint(g2); final long time2 = System.currentTimeMillis(); drawSectionBorders(g2); final long time3 = System.currentTimeMillis(); final int cnt4 = drawPipeParts(g2, p, clipOrg); final long time4 = System.currentTimeMillis(); final int cnt5 = drawConnections(g2, p, clipOrg); final long time5 = System.currentTimeMillis(); drawAutoLayoutInfo(g2, p.layouter); drawPipeFlow(g2, p.pipeflow); if (BoardConfiguration.CFG_PAINT_DEBUG.getContent()) { g2.translate(clip.x / scale, clip.y / scale); drawDebugTime(g2, time1 - start, -1, "Background", 1); drawDebugTime(g2, time2 - time1, -1, "Hint", 2); drawDebugTime(g2, time3 - time2, -1, "Border", 3); drawDebugTime(g2, time4 - time3, cnt4, "Parts", 4); drawDebugTime(g2, time5 - time4, cnt5, "Conn's", 5); } p.g.drawImage(img, clip.x, clip.y, null); } private void drawDebugTime(final Graphics2D g2, final long elapsed, final int count, final String name, final int pos) { final Font f = g2.getFont(); g2.setFont(f.deriveFont(10f)); g2.setColor(Color.GREEN); final String str = count == -1 ? name : String.format("%d %s", count, name); g2.drawString(String.format("%d ms for %s", elapsed, str), 0, 10 * pos); g2.setFont(f); } private void drawMovementHint(final Graphics2D g2, final PaintParameters p) { final List<? extends PipePart> list; final int x0; final int x1; if (p.currentPart instanceof Input) { list = pipe.getInputList(); x0 = 0; x1 = border1; } else if (p.currentPart instanceof Converter) { list = pipe.getConverterList(); x0 = border1; x1 = border2; } else if (p.currentPart instanceof Output) { list = pipe.getOutputList(); x0 = border2; x1 = (int) (bounds.width / scale); } else return; final int idx = list.indexOf(p.currentPart); final PipePart before = idx > 0 ? list.get(idx - 1) : null; final PipePart after = idx < list.size() - 1 ? list.get(idx + 1) : null; final int y0 = before == null ? 0 : before.getGuiPosition().getY() + before.getGuiPosition().getHeight() + 1; final int y1 = after == null ? (int) (bounds.height / scale) : after .getGuiPosition().getY() - 1; final Rectangle r = new Rectangle(x0, y0, x1 - x0, y1 - y0); if (BoardConfiguration.CFG_PAINT_DEBUG.getContent()) g2.setColor(new Color(grayVal, grayVal + 16, grayVal)); else g2.setColor(BoardConfiguration.CFG_MOVEMENT_HINT.getContent()); g2.fill(r); } private void drawOrderingHint(final Graphics2D g2) { final double w = border1; final double h = bounds.height / scale; final int tw = (int) (w * BoardConfiguration.CFG_ORDER_HINT_TRUNK_WIDTH .getContent()); final int th = (int) (h * BoardConfiguration.CFG_ORDER_HINT_TRUNK_HEIGHT .getContent()); final int aw = (int) (w * BoardConfiguration.CFG_ORDER_HINT_ARROW_WIDTH .getContent()); final int ah = (int) (h * BoardConfiguration.CFG_ORDER_HINT_ARROW_HEIGHT .getContent()); final int cw = tw + 2 * aw; final int ch = th + ah; final int ow = (int) ((w - cw) / 2); final int oh = (int) ((h - ch) / 2); final Polygon poly = new Polygon(); poly.addPoint(ow + aw, oh); poly.addPoint(ow + aw + tw, oh); poly.addPoint(ow + aw + tw, oh + th); poly.addPoint(ow + aw + tw + aw, oh + th); poly.addPoint(ow + aw + tw / 2, oh + th + ah); poly.addPoint(ow, oh + th); poly.addPoint(ow + aw, oh + th); poly.addPoint(ow + aw, oh); if (BoardConfiguration.CFG_SHADOW_ORDERHINT.getContent() && BoardConfiguration.CFG_SHADOW_DEPTH.getContent() > 0) { final AffineTransform at = g2.getTransform(); g2.translate(BoardConfiguration.CFG_SHADOW_DEPTH.getContent(), BoardConfiguration.CFG_SHADOW_DEPTH.getContent()); g2.setColor(BoardConfiguration.CFG_SHADOW_COLOR.getContent()); g2.fillPolygon(poly); g2.setTransform(at); } g2.setColor(BoardConfiguration.CFG_ORDER_HINT_BACK.getContent()); g2.fillPolygon(poly); } private void drawSectionBorders(final Graphics2D g2) { g2.setStroke(new BasicStroke(2, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 0, new float[] { 3, 3 }, 0)); g2.setColor(BoardConfiguration.CFG_SECT_BORDER.getContent()); final int h = (int) (bounds.height / scale + 0.5); g2.drawLine(border1, 0, border1, h); g2.drawLine(border2, 0, border2, h); } private int drawPipeParts(final Graphics2D g2, final PaintParameters p, final Rectangle clip) { int cnt = 0; g2.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 0, null, 0)); for (final PipePart pp : set) cnt += drawPipePart(g2, p, pp, clip); return cnt; } private int drawConnections(final Graphics2D g2, final PaintParameters p, final Rectangle clip) { int cnt = 0; for (final PipePart src : set) for (final PipePart trg : src.getConnectedPipeParts()) { final boolean sel = p.currentPart == src && p.currentConnectionsTarget == trg; if (saneConfigCache.get(src) == null) g2.setColor(sel ? BoardConfiguration.CFG_CONNECTION_SEL_OK .getContent() : BoardConfiguration.CFG_CONNECTION_OK.getContent()); else g2.setColor(sel ? BoardConfiguration.CFG_CONNECTION_SEL_BAD .getContent() : BoardConfiguration.CFG_CONNECTION_BAD .getContent()); cnt += drawConnection(g2, src.getGuiPosition(), trg.getGuiPosition(), src, trg, clip); } if (p.currentConnection != null && p.currentConnectionsTarget == null) { g2.setColor(p.currentConnectionValid ? BoardConfiguration.CFG_CONNECTION_SEL_OK .getContent() : BoardConfiguration.CFG_CONNECTION_SEL_BAD .getContent()); cnt += drawConnection(g2, p.currentPart.getGuiPosition(), new ImmutableRectangle(p.currentConnection), p.currentPart, p.underCursor, clip); } return cnt; } private int drawPipePart(final Graphics2D g2, final PaintParameters p, final PipePart part, final Rectangle clip) { final boolean hover = part == p.underCursor; final ImmutableRectangle rect = part.getGuiPosition(); if (BoardConfiguration.CFG_SHADOW_RECTS.getContent() && BoardConfiguration.CFG_SHADOW_DEPTH.getContent() > 0) { final Rectangle r = rect.createCopy(); r.width += BoardConfiguration.CFG_SHADOW_DEPTH.getContent(); r.height += BoardConfiguration.CFG_SHADOW_DEPTH.getContent(); if (!r.intersects(clip)) return 0; } else if (!rect.intersects(clip)) return 0; final Color outerClr; if (saneConfigCache.get(part) == null) outerClr = part == p.currentPart ? BoardConfiguration.CFG_OUTER_SEL_OK .getContent() : BoardConfiguration.CFG_OUTER_OK .getContent(); else outerClr = part == p.currentPart ? BoardConfiguration.CFG_OUTER_SEL_BAD .getContent() : BoardConfiguration.CFG_OUTER_BAD .getContent(); if (BoardConfiguration.CFG_SHADOW_RECTS.getContent() && BoardConfiguration.CFG_SHADOW_DEPTH.getContent() > 0) { final AffineTransform at = g2.getTransform(); g2.translate(BoardConfiguration.CFG_SHADOW_DEPTH.getContent(), BoardConfiguration.CFG_SHADOW_DEPTH.getContent()); g2.setColor(BoardConfiguration.CFG_SHADOW_COLOR.getContent()); rect.fill(g2); g2.setTransform(at); } g2.setColor(BoardConfiguration.CFG_RECT_BACKGROUND.getContent()); rect.fill(g2); g2.setColor(outerClr); rect.draw(g2); final String s = BoardConfiguration.CFG_DRAW_SHORTCONFIG.getContent() ? part .getShortConfigDescr() : part.getName(); final Rectangle2D sb = g2.getFontMetrics().getStringBounds(s, g2); if (hover) { final Rectangle inner = rect.createCopy(); inner.grow(-BoardConfiguration.CFG_ICON_WIDTH.getContent() - BoardConfiguration.CFG_INNER_WIDTH.getContent(), -BoardConfiguration.CFG_INNER_HEIGHT.getContent()); g2.setColor(p.modifyable ? BoardConfiguration.CFG_INNER_MODIFIABLE .getContent() : BoardConfiguration.CFG_INNER_READONLY .getContent()); g2.fill(inner); } // restrict drawing to inside the rectangle final Shape shape = g2.getClip(); rect.asClip(g2); // draw main icon Icon mainIcon = part.getIcon(); if (mainIcon == null) { final BufferedImage img = new BufferedImage(rect.getHeight(), rect.getHeight(), BufferedImage.TYPE_INT_ARGB); final Icon cfgImage = part.getConfigImage(); if (cfgImage instanceof ImageIcon) { final Graphics2D g = img.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g.drawImage(((ImageIcon) cfgImage).getImage(), 0, 0, rect.getHeight(), rect.getHeight(), null); mainIcon = new ImageIcon(img); } else mainIcon = IconLoader.getMissingIcon(); } mainIcon.paintIcon(null, g2, rect.getX(), rect.getY() + (rect.getHeight() - mainIcon.getIconHeight()) / 2); // draw name of PipePart g2.setColor(outerClr); g2.drawString(s, (float) (rect.getX() + (rect.getWidth() - sb.getWidth()) / 2), (float) (rect.getY() + sb.getHeight())); // draw clickable icons drawIcon(g2, hover && p.currentIcon == ICON_CONF, rect, ICON_CONF, BoardConfiguration.CFG_ICON_CONF_POS.getContent(), !p.modifyable || part.getGuiConfigs().isEmpty(), false, p.modifyable); drawIcon(g2, hover && p.currentIcon == ICON_DGR, rect, ICON_DGR, BoardConfiguration.CFG_ICON_DGR_POS.getContent(), false, part.isVisualize(), p.modifyable); // restore old clip g2.setClip(shape); return 1; } /** * Draws an icon to the image * * @param g2 * {@link Graphics2D} of the image * @param hover * if true, an outline will be drawn * @param rect * position of the image in which to align the icon * @param icon * {@link Icon} to draw * @param pos * the position inside the rectangle as follow<br> * [ 1 2 3 ..... -2 -1 0 ] * @param disabled * if true, icon is drawn in disabled state * @param selected * if true, icon is drawn in selected state * @param modifyable * determines the color of the icon's background */ private void drawIcon(final Graphics2D g2, final boolean hover, final ImmutableRectangle rect, final Icon icon, final int pos, final boolean disabled, final boolean selected, final boolean modifyable) { final Rectangle b = getIconBounds(rect, icon, pos); if (hover) { g2.setColor(modifyable ? BoardConfiguration.CFG_INNER_MODIFIABLE .getContent() : BoardConfiguration.CFG_INNER_READONLY .getContent()); g2.fill(b); } Icon ico = disabled ? (selected ? UIManager.getLookAndFeel() .getDisabledSelectedIcon(null, icon) : UIManager .getLookAndFeel().getDisabledIcon(null, icon)) : icon; if (ico == null) ico = icon; ico.paintIcon(null, g2, b.x, b.y); if (hover) g2.setColor(selected ? BoardConfiguration.CFG_ICON_OUTLINE_SEL_HOVER .getContent() : BoardConfiguration.CFG_ICON_OUTLINE_HOVER .getContent()); else g2.setColor(selected ? BoardConfiguration.CFG_ICON_OUTLINE_SEL .getContent() : BoardConfiguration.CFG_ICON_OUTLINE .getContent()); g2.draw3DRect(b.x, b.y, b.width, b.height, !selected); } private int drawConnection(final Graphics2D g2, final ImmutableRectangle srcRect, final ImmutableRectangle trgRect, final PipePart src, final PipePart trg, final Rectangle clip) { if (!srcRect.union(trgRect.createCopy()).intersects(clip)) return 0; final Point srcPoint = new Point(); final Point trgPoint = new Point(); calcConnectorPositions(srcRect, trgRect, srcPoint, trgPoint); final Polygon arrow = getArrowPolygon(srcPoint.x, srcPoint.y, trgPoint.x, trgPoint.y); if (BoardConfiguration.CFG_SHADOW_CONNECTIONS.getContent() && BoardConfiguration.CFG_SHADOW_DEPTH.getContent() > 0) { final Color clr = g2.getColor(); final AffineTransform at = g2.getTransform(); g2.translate(BoardConfiguration.CFG_SHADOW_DEPTH.getContent() / 2, BoardConfiguration.CFG_SHADOW_DEPTH.getContent() / 2); g2.setColor(BoardConfiguration.CFG_SHADOW_COLOR.getContent()); g2.drawLine(srcPoint.x, srcPoint.y, trgPoint.x, trgPoint.y); g2.fillPolygon(arrow); drawConnectorLabel(g2, srcPoint.x, srcPoint.y, trgPoint.x, trgPoint.y, BoardConfiguration.CFG_SHADOW_CONNECTIONS_LABEL .getContent() ? src.getOutputDescription() : ""); drawConnectorLabel( g2, trgPoint.x, trgPoint.y, srcPoint.x, srcPoint.y, trg == null || !BoardConfiguration.CFG_SHADOW_CONNECTIONS_LABEL .getContent() ? "" : trg .getInputDescription()); g2.setTransform(at); g2.setColor(clr); } g2.drawLine(srcPoint.x, srcPoint.y, trgPoint.x, trgPoint.y); g2.fillPolygon(arrow); drawConnectorLabel(g2, srcPoint.x, srcPoint.y, trgPoint.x, trgPoint.y, src.getOutputDescription()); drawConnectorLabel(g2, trgPoint.x, trgPoint.y, srcPoint.x, srcPoint.y, trg == null ? "" : trg.getInputDescription()); return 1; } private void drawConnectorLabel(final Graphics2D g2, final int sx, final int sy, final int tx, final int ty, final String str) { if (str.isEmpty()) return; // draw in image final Rectangle sb = g2.getFontMetrics().getStringBounds(str, g2) .getBounds(); final int sw = (int) (sb.width * scale); final int sh = (int) (sb.height * scale); if (sw <= 0 || sh <= 0) return; final BufferedImage img = new BufferedImage(sw, sh, BufferedImage.TYPE_INT_ARGB); final Graphics2D imgG2D = img.createGraphics(); imgG2D.scale(scale, scale); imgG2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); if (BoardConfiguration.CFG_PAINT_DEBUG.getContent()) { imgG2D.setColor(Color.YELLOW); imgG2D.fillRect(0, 0, sb.width, sb.height); } imgG2D.setColor(g2.getColor()); imgG2D.setFont(g2.getFont()); imgG2D.drawString(str, 0, -sb.y); // make sure text is never bottom-up and correctly positioned double d = Math.atan2(sy - ty, sx - tx); int xoffs = 16; if (d > Math.PI / 2) d -= Math.PI; else if (d < -Math.PI / 2.0) d += Math.PI; else xoffs = (int) (-sb.width * scale - xoffs); // draw rotated image final Shape shape = g2.getClip(); final AffineTransform at = g2.getTransform(); // only use translation from original transformation // scaling is already done in imgG2D g2.setTransform(new AffineTransform()); g2.translate(at.getTranslateX() + sx * scale, at.getTranslateY() + sy * scale); g2.rotate(d); final int len = (int) (Math.sqrt((sx - tx) * (sx - tx) + (sy - ty) * (sy - ty)) * scale); if (xoffs < 0) g2.clipRect(-len / 2, 0, len / 2, sh); else g2.clipRect(0, 0, len / 2, sh); g2.drawImage(img, new AffineTransformOp(new AffineTransform(), AffineTransformOp.TYPE_BILINEAR), xoffs, 0); g2.setTransform(at); g2.setClip(shape); } private void drawAutoLayoutInfo(final Graphics2D g2, final BoardAutoLayouter layouter) { if (layouter == null) return; final int w = bounds.width * 2 / 3; final int h = 20; final int x = (bounds.width - w) / 2; final int y = (bounds.height - h) / 2; g2.setColor(Color.LIGHT_GRAY); g2.fill3DRect(x, y, w, h, false); g2.setColor(new Color(128, 128, 255)); g2.fill3DRect(x + 1, y + 1, (int) (w * layouter.getProgress()) - 1, h - 2, true); g2.setColor(Color.BLACK); final String s = layouter.getProgressText(); final Rectangle2D sb = g2.getFontMetrics().getStringBounds(s, g2); g2.drawString(s, (int) ((bounds.width - sb.getWidth()) / 2), (int) (y + h - (h - sb.getHeight()) / 2)); } private void drawPipeFlow(final Graphics2D g2, final Collection<PipeFlow> pipeflow) { if (pipeflow != null) synchronized (pipeflow) { g2.setStroke(FLOW_STROKE); for (final PipeFlow pf : pipeflow) pf.draw(g2); } } public static void calcConnectorPositions(final ImmutableRectangle srcRect, final ImmutableRectangle trgRect, final Point srcPos, final Point trgPos) { // calculate center of source and target final int csx = srcRect.getX() + srcRect.getWidth() / 2; final int csy = srcRect.getY() + srcRect.getHeight() / 2; final int ctx = trgRect.getX() + trgRect.getWidth() / 2; final int cty = trgRect.getY() + trgRect.getHeight() / 2; // determine side of rectangle for the connection final int dcx = ctx - csx; final int dcy = cty - csy; if (dcx > Math.abs(dcy)) { // target is right of source if (srcPos != null) srcPos.setLocation(srcRect.getX() + srcRect.getWidth(), csy); if (trgPos != null) trgPos.setLocation(trgRect.getX(), cty); } else if (dcy >= Math.abs(dcx)) { // target is below source if (srcPos != null) srcPos.setLocation(csx, srcRect.getY() + srcRect.getHeight()); if (trgPos != null) trgPos.setLocation(ctx, trgRect.getY()); } else if (dcx < 0 && Math.abs(dcx) > Math.abs(dcy)) { // target is left of source if (srcPos != null) srcPos.setLocation(srcRect.getX(), csy); if (trgPos != null) trgPos.setLocation(trgRect.getX() + trgRect.getWidth(), cty); } else { // target is above source if (srcPos != null) srcPos.setLocation(csx, srcRect.getY()); if (trgPos != null) trgPos.setLocation(ctx, trgRect.getY() + trgRect.getHeight()); } } // from http://www.java-forums.org/awt-swing/ // 5842-how-draw-arrow-mark-using-java-swing.html public static Polygon getArrowPolygon(final int sx, final int sy, final int tx, final int ty) { final Polygon polygon = new Polygon(); final double d = Math.atan2(sx - tx, sy - ty); // tip polygon.addPoint(tx, ty); // wing 1 polygon.addPoint( tx + xCor(BoardConfiguration.CFG_CONN_ARROW_HEAD .getContent(), d + .5), ty + yCor(BoardConfiguration.CFG_CONN_ARROW_HEAD .getContent(), d + .5)); // on line polygon.addPoint( tx + xCor(BoardConfiguration.CFG_CONN_ARROW_WING .getContent(), d), ty + yCor(BoardConfiguration.CFG_CONN_ARROW_WING .getContent(), d)); // wing 2 polygon.addPoint( tx + xCor(BoardConfiguration.CFG_CONN_ARROW_HEAD .getContent(), d - .5), ty + yCor(BoardConfiguration.CFG_CONN_ARROW_HEAD .getContent(), d - .5)); // back to tip, close polygon polygon.addPoint(tx, ty); return polygon; } private static int xCor(final int len, final double dir) { return (int) (len * Math.sin(dir)); } private static int yCor(final int len, final double dir) { return (int) (len * Math.cos(dir)); } /** * Gets the bounding rectangle around an icon. * * @param rect * position of the image in which to align the icon * @param icon * {@link Icon} which would be drawn * @param pos * the position inside the rectangle as follow<br> * [ 1 2 3 ..... -2 -1 0 ] * @return the bounding rectangle */ private static Rectangle getIconBounds(final ImmutableRectangle rect, final Icon icon, final int pos) { final boolean alignRight = pos <= 0; final Rectangle b = new Rectangle(); b.width = icon.getIconWidth(); b.height = icon.getIconHeight(); if (alignRight) b.x = rect.getX() + rect.getWidth() - (1 - pos) * BoardConfiguration.CFG_ICON_WIDTH.getContent(); else b.x = rect.getX() + pos * BoardConfiguration.CFG_ICON_WIDTH.getContent() - b.width; b.y = rect.getY() + rect.getHeight() - BoardConfiguration.CFG_ICON_WIDTH.getContent(); return b; } public static Object getPipePartElement(final PipePart pp, final Point position) { final Rectangle inner = pp.getGuiPosition().createCopy(); inner.grow(-BoardConfiguration.CFG_ICON_WIDTH.getContent() - BoardConfiguration.CFG_INNER_WIDTH.getContent(), -BoardConfiguration.CFG_INNER_HEIGHT.getContent()); final Rectangle ibC = getIconBounds(pp.getGuiPosition(), ICON_CONF, BoardConfiguration.CFG_ICON_CONF_POS.getContent()); final Rectangle ibD = getIconBounds(pp.getGuiPosition(), ICON_DGR, BoardConfiguration.CFG_ICON_DGR_POS.getContent()); if (ibC.contains(position)) return ICON_CONF; else if (ibD.contains(position)) return ICON_DGR; else if (inner.contains(position)) return new Point(position.x - pp.getGuiPosition().getX(), position.y - pp.getGuiPosition().getY()); else return null; } public void setPipe(final Pipe pipe, final Graphics g, final boolean allowMoving) { this.pipe = pipe; set.clear(); for (final PipePart pp : pipe.getInputList()) addToSet(pp, g, allowMoving); for (final PipePart pp : pipe.getConverterList()) addToSet(pp, g, allowMoving); for (final PipePart pp : pipe.getOutputList()) addToSet(pp, g, allowMoving); updateSaneConfigCache0(); } public Pipe getPipe() { return pipe; } void addToSet(final PipePart pp, final Graphics g, final boolean allowMoving) { set.add(pp); if (allowMoving) { final Rectangle r = pp.getGuiPosition().createCopy(); r.height = g.getFontMetrics().getHeight() + BoardConfiguration.CFG_ICON_WIDTH.getContent() + 2; r.width = 0; check(r, pp); pp.setGuiPosition(r); recalculatePipePartWidth(pp, g); } } final void recalculatePipePartWidth(final PipePart pp, final Graphics g) { final String label = BoardConfiguration.CFG_DRAW_SHORTCONFIG .getContent() ? pp.getShortConfigDescr() : pp.getName(); final int txtWidth = (int) (g.getFontMetrics() .getStringBounds(label, g).getWidth() + 0.99); final Rectangle r = pp.getGuiPosition().createCopy(); r.width = Math.min(BoardConfiguration.CFG_MAX_RECT_WIDTH.getContent(), Math.max(BoardConfiguration.CFG_ICON_WIDTH.getContent() * BoardConfiguration.CFG_ICON_MAX.getContent(), txtWidth + r.height * 2)); r.width = r.width / 10 * 10; check(r, pp); pp.setGuiPosition(r); } public Set<PipePart> getSet() { return set; } public Map<PipePart, String> getSaneConfigCache() { return saneConfigCache; } private boolean updateSaneConfigCache0() { final Map<PipePart, String> sane = pipe.getSanePipeParts(); if (saneConfigCache.equals(sane)) return false; saneConfigCache.clear(); saneConfigCache.putAll(sane); return true; } public boolean updateSaneConfigCache() { final boolean modified = updateSaneConfigCache0(); if (modified) pipe.modified(); return modified; } public Dimension getPreferredSize() { // get lower right corner int x = 0; int y = 0; for (final PipePart pp : set) { final ImmutableRectangle r = pp.getGuiPosition(); x = Math.max(x, r.getX() + r.getWidth()); y = Math.max(y, r.getY() + r.getHeight()); } // add some free space and consider scaling return new Dimension(Math.max(BoardConfiguration.CFG_PREFSIZE_MIN .getContent(), (int) (x * scale + BoardConfiguration.CFG_PREFSIZE_FREE .getContent())), Math.max( BoardConfiguration.CFG_PREFSIZE_MIN.getContent(), (int) (y * scale + BoardConfiguration.CFG_PREFSIZE_FREE .getContent()))); } public Dimension getBounds() { return bounds; } public void setBounds(final int width, final int height, final boolean allowMoving) { bounds.setSize(width, height); border1 = Math.min( width / BoardConfiguration.CFG_SECTION_FRAG.getContent(), BoardConfiguration.CFG_MAX_RECT_WIDTH.getContent() + BoardConfiguration.CFG_SECTION_SPACE.getContent()); border2 = width - border1; if (allowMoving) for (final PipePart pp : set) { final Rectangle r = pp.getGuiPosition().createCopy(); check(r, pp); pp.setGuiPosition(r); } } public int getBorder1(final boolean considerScaling) { return considerScaling ? (int) (border1 * scale) : border1; } public int getBorder2(final boolean considerScaling) { return considerScaling ? (int) (border2 * scale) : border2; } public double getScale() { return scale; } public void setScale(final double scale) { this.scale = scale; } void check(final Rectangle r, final PipePart pp) { final int xMin; final int xMax; final int yMin = 1; final int yMax = (int) (bounds.height / scale) - 1; if (Input.class.isInstance(pp)) { xMin = 1; xMax = border1 - 1; } else if (Converter.class.isInstance(pp)) { xMin = border1 + 1; xMax = border2 - 1; } else if (Output.class.isInstance(pp)) { xMin = border2 + 1; xMax = (int) (bounds.getWidth() / scale) - 1; } else { xMin = 1; xMax = (int) (bounds.getWidth() / scale) - 1; } if (r.x < xMin) r.x = xMin; if (r.y < yMin) r.y = yMin; if (r.x + r.getWidth() > xMax) r.x = xMax - r.width; if (r.y + r.height > yMax) r.y = yMax - r.height; for (final PipePart other : set) if (other != pp && other.getGuiPosition().intersects(r)) { // move r, so it doesn't intersect anymore final ImmutableRectangle rO = other.getGuiPosition(); final Rectangle i = rO.intersection(r); final int x0 = r.x + r.width / 2; final int y0 = r.y + r.height / 2; final int x1 = rO.getX() + rO.getWidth() / 2; final int y1 = rO.getY() + rO.getHeight() / 2; if (i.getWidth() < i.height && r.x - i.getWidth() >= xMin && r.x + r.getWidth() + i.getWidth() <= xMax) { if (x0 > x1) // move right r.translate(i.width, 0); else // move left r.translate(-i.width, 0); } else if (y0 > y1) { if (r.getY() + r.height + i.height > yMax) // move up instead of down r.translate(0, i.height - r.height - rO.getHeight()); else r.translate(0, i.height); // move down } else if (r.getY() - i.height < yMin) // move down instead of up r.translate(0, r.height + rO.getHeight() - i.height); else r.translate(0, -i.height); // move up // check bounds again // (overlapping is better than being out of bounds) if (r.x < xMin) r.x = xMin; if (r.getY() < yMin) r.y = yMin; if (r.x + r.getWidth() > xMax) r.x = xMax - r.width; if (r.getY() + r.height > yMax) r.y = yMax - r.height; } } }