/******************************************************************************* * Copyright 2012 Pearson Education * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package org.semantictools.context.renderer; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.geom.Ellipse2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import javax.imageio.ImageIO; import org.semantictools.context.renderer.model.DiagramSpec; import org.semantictools.context.renderer.model.Modifier; import org.semantictools.context.renderer.model.Node; import org.semantictools.context.renderer.model.Rect; import org.semantictools.graphics.Padding; public class ContextRenderer { private Style style; private FontMetrics nameMetrics; private FontMetrics typeMetrics; private StreamFactory streamFactory; public ContextRenderer(StreamFactory factory) { this.streamFactory = factory; style = new Style(true); } public void renderGraphicalNotationFigure(DiagramSpec spec) throws IOException { computeLayout(spec); NotationPainter painter = new NotationPainter(spec); painter.paintImage(); } public void render(DiagramSpec spec) throws IOException { computeLayout(spec); paint(spec); } private void paint(DiagramSpec spec) throws IOException { paintRootNode(spec); } private void paintRootNode(DiagramSpec spec) throws IOException { Painter painter = new Painter(spec); painter.paintImage(); } private void computeLayout(DiagramSpec spec) { LayoutEngine engine = new LayoutEngine(spec); engine.computeLayout(); } class NotationPainter extends Painter { private static final int ARROW_LENGTH = 50; private static final int ARROW_HEAD_LENGTH = 4; private static final int ARROW_SPACING = 5; private static final String PROPERTY_NAME = "property name"; private static final String PROPERTY_TYPE = "property type"; private Rectangle2D propertyNameBounds; private Rectangle2D propertyTypeBounds; private int ascent; NotationPainter(DiagramSpec spec) { super(); init(spec); } protected void computeImageDimensions() { super.computeImageDimensions(); BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); FontMetrics metrics = g.getFontMetrics( style.getLabelFont() ); ascent = metrics.getAscent()/2 - metrics.getDescent(); propertyNameBounds = metrics.getStringBounds(PROPERTY_NAME, g); propertyTypeBounds = metrics.getStringBounds(PROPERTY_TYPE, g); int textWidth = (int)Math.max(propertyNameBounds.getWidth(), propertyTypeBounds.getWidth()); imageWidth += ARROW_LENGTH + 2*ARROW_HEAD_LENGTH + 3*ARROW_SPACING + textWidth; } public void paintImage() throws IOException { paintLabeledArrow(spec.getRoot().getNameRect(), propertyNameBounds, PROPERTY_NAME); paintLabeledArrow(spec.getRoot().getTypeRect(), propertyTypeBounds, PROPERTY_TYPE); super.paintImage(); } private void paintLabeledArrow(Rect targetRect, Rectangle2D labelBounds, String labelText) { int x0 = targetRect.getWidth() + ARROW_SPACING; int y0 = targetRect.getY() + targetRect.getHeight()/2; int x1 = x0 + ARROW_HEAD_LENGTH*2; int y1 = y0 + ARROW_HEAD_LENGTH; int x2 = x1; int y2 = y0 - ARROW_HEAD_LENGTH; int x[] = new int[] {x0, x1, x2, x0}; int y[] = new int[] {y0, y1, y2, y0}; Stroke width1 = new BasicStroke(1); Stroke width2 = new BasicStroke(2); graphics.setColor(Color.red); graphics.setPaint(Color.red); graphics.setStroke(width1); graphics.fillPolygon(x, y, 3); graphics.drawPolygon(x, y, 3); Stroke stroke = graphics.getStroke(); graphics.setStroke(width2); x0 = x1; x1 = x0 + ARROW_LENGTH; y1 = y0; graphics.drawLine(x0, y0, x1, y1); graphics.setStroke(stroke); graphics.setColor(Color.black); graphics.setFont(style.getLabelFont()); x0 = x1 + 2*ARROW_SPACING; y0 = y0 + ascent; graphics.drawString(labelText, x0, y0); } } private class Painter { BufferedImage image; Graphics2D graphics; DiagramSpec spec; protected int imageWidth; protected int imageHeight; protected Painter() { } Painter(DiagramSpec spec) { init(spec); } protected void init(DiagramSpec spec) { this.spec = spec; computeImageDimensions(); image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB); graphics = image.createGraphics(); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } protected void computeImageDimensions() { updateDimensions(spec.getRoot()); } private void updateDimensions(Node node) { imageWidth = Math.max(imageWidth, node.getRight()+1); imageHeight = Math.max(imageHeight, node.getBottom()+1); if (node.getChildren() != null) { for (Node n : node.getChildren()) { updateDimensions(n); } } } public void paintImage() throws IOException { paintNode(spec.getRoot()); writeFile(); } private void writeFile() throws IOException { OutputStream stream = streamFactory.createOutputStream(spec.getImagePath()); try { ImageIO.write(image, "png", stream); } finally { stream.close(); } } private void paintNode(Node node) { graphics.setColor( style.getBoxBorderColor() ); drawText( node.getTypeText(), node.getTypeRect(), style.getTypePadding(), style.getTypeFont(), style.getTypeTextColor(), style.getTypeBgColor()); drawText( node.getNameText(), node.getNameRect(), style.getNamePadding(), style.getNameFont(), style.getNameTextColor(), style.getNameBgColor()); String q = node.getNameQualifier(); if (q!=null) { drawText( q, node.getNameRect(), style.getNamePadding(), style.getNameFont(), style.getArcColor(), null ); } drawRect(node.getOutline(), style.getBoxBorderColor()); drawModifier(node); if (node.getChildren() != null) { for (Node n : node.getChildren()) { paintNode(n); } paintArc(node); } } private void drawModifier(Node node) { if (node.getModifier() == Modifier.NONE) return; int diameter = style.getModifierDiameter(); int middle = (node.getTop() + node.getBottom()) / 2; int x = node.getLeft(); int y = middle - diameter/2; Ellipse2D.Float circle = new Ellipse2D.Float(x, y, diameter, diameter); graphics.setPaint(style.getTypeBgColor()); graphics.fill(circle); graphics.setPaint(style.getBoxBorderColor()); graphics.draw(circle); String symbol = node.getModifier().getSymbol(); Rectangle2D bounds = nameMetrics.getStringBounds(symbol, graphics); int height = (int) bounds.getHeight(); int width = (int) bounds.getWidth(); // Compute the coordinates of the symbol so that it is centered in the circle x = x + (diameter - width)/2 + nameMetrics.getLeading(); y = y + (diameter - height)/2 + nameMetrics.getAscent() + nameMetrics.getLeading(); graphics.setFont(style.getNameFont()); graphics.setColor(style.getArcColor()); graphics.drawString(symbol, x, y); } private void paintArc(Node node) { switch (node.getBranchStyle()) { case RECTILINEAR : paintRectilinearArc(node); break; case OBLIQUE: paintObliqueArc(node); break; } } private void paintObliqueArc(Node node) { int top = node.getTop(); int bottom = node.getBottom(); int x0 = node.getRight(); int y0 = (top+bottom)/2; Node firstChild = node.getFirstChild(); int childLeft = firstChild.getLeft(); int x2 = childLeft; int y2 = firstChild.getTop() + firstChild.getHeight()/2; paintArc(x0, y0, x2, y2); Node lastChild = node.getLastChild(); if (lastChild == firstChild) return; y2 = lastChild.getTop() + lastChild.getHeight()/2; paintArc(x0, y0, x2, y2); } private void paintRectilinearArc(Node node) { int top = node.getTop(); int bottom = node.getBottom(); int x0 = node.getRight(); int y0 = (top+bottom)/2; Node firstChild = node.getFirstChild(); int childLeft = firstChild.getLeft(); int hspace = style.getHorizontalSpacing()/2; int x1 = childLeft - hspace; int y1 = y0; paintArc(x0, y0, x1, y1); int x2 = childLeft; int y2 = firstChild.getTop() + firstChild.getHeight()/2; paintArc(x1, y1, x1, y2); paintArc(x1, y2, x2, y2); Node lastChild = node.getLastChild(); if (lastChild == firstChild) return; y2 = lastChild.getTop() + lastChild.getHeight()/2; paintArc(x1, y1, x1, y2); paintArc(x1, y2, x2, y2); } private void paintArc(int x0, int y0, int x1, int y1) { graphics.setPaint(style.getArcColor()); graphics.drawLine(x0, y0, x1, y1); } private void fill(Rect box, Color bgColor) { Rectangle2D.Float shape = new Rectangle2D.Float(); shape.setRect(box.getX(), box.getY(), box.getWidth(), box.getHeight()); graphics.setPaint(bgColor); graphics.fill(shape); } @SuppressWarnings("deprecation") private void drawText(String text, Rect box, Padding padding, Font font, Color color, Color bgColor) { int x = box.getX() + padding.getPadLeft(); int y = box.getY() + box.getHeight() - padding.getPadBottom() - padding.getMaxDescent(); if (bgColor != null) { fill(box, bgColor); graphics.setBackground(bgColor); } graphics.setFont(font); graphics.setColor(color); graphics.drawString(text, x, y); } public void drawRect(Rect box, Color color) { int x = box.getX(); int y = box.getY(); int width = box.getWidth(); int height = box.getHeight(); graphics.setColor(color); graphics.drawRect(x, y, width, height); } } private class LayoutEngine { private DiagramSpec spec; private int nameHeight; private int typeHeight; private Grid grid = new Grid(); @SuppressWarnings("deprecation") LayoutEngine(DiagramSpec spec) { this.spec = spec; BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); nameMetrics = g.getFontMetrics( style.getNameFont() ); typeMetrics = g.getFontMetrics( style.getTypeFont() ); Padding namePad = style.getNamePadding(); Padding typePad = style.getTypePadding(); nameHeight = (int)(nameMetrics.getHeight() + namePad.getPadTop() + namePad.getPadBottom()); typeHeight = (int)(typeMetrics.getHeight() + typePad.getPadTop() + typePad.getPadBottom()); style.setModifierDiameter(typeHeight); namePad.setMaxDescent(nameMetrics.getMaxDescent()); typePad.setMaxDescent(typeMetrics.getMaxDescent()); } public void computeLayout() { Node root = spec.getRoot(); grid.inject(root); computeDimensions(root); root.setLeft(0, style.getModifierDiameter()); grid.computeHorizontalSpacing(style.getHorizontalSpacing()); grid.computeVerticalSpacing(style.getVerticalSpacing()); grid.refinePlacement(style.getVerticalSpacing()); } public void computeDimensions(Node node) { String localName = node.getNameText(); String typeName = node.getTypeText(); setDimensions(node.getNameRect(), localName, nameHeight, nameMetrics, style.getNamePadding()); setDimensions(node.getTypeRect(), typeName, typeHeight, typeMetrics, style.getTypePadding()); node.alignWidth(style.getModifierDiameter()); node.computeHeight(); List<Node> kids = node.getChildren(); if (kids != null) { for (Node n : kids) { computeDimensions(n); } } } private void setDimensions(Rect rect, String text, int height, FontMetrics metrics, Padding padding) { rect.setHeight(height); rect.setWidth((int)(metrics.stringWidth(text) + padding.getPadLeft() + padding.getPadRight())); } } class Column { List<Node> nodeList = new ArrayList<Node>(); Rect boundary = new Rect(); public List<Node> getNodeList() { return nodeList; } public Rect getBoundary() { return boundary; } public void add(Node node) { nodeList.add(node); } public void setLeft(int left) { boundary.setX(left); for (Node n : nodeList) { n.setLeft(left, style.getModifierDiameter()); } } } class Grid { List<Column> columnList = new ArrayList<Column>(); public Node getNode(int column, int index) { return columnList.get(column).getNodeList().get(index); } public void refinePlacement(int spacing) { Node root = getNode(0, 0); refinePlacement(root, spacing); } public Column getColumn(int col) { return columnList.get(col); } private void refinePlacement(Node node, int spacing) { int dy = getDeltaY(node, spacing); if (dy > 0) { shiftColumn(node, dy, spacing); } List<Node> kids = node.getChildren(); if (kids != null) { for (Node n : kids){ refinePlacement(n, spacing); } } } private int getDeltaY(Node node, int spacing) { int row = node.getRow(); int column = node.getColumn(); int top = node.getBoundary().getY(); int prevTop = 0; if (row > 0) { Node prev = getNode(column, row-1); prevTop = prev.getBottom() + spacing; } int dy = prevTop - top; return dy; } private void shiftColumn(Node node, int dy, int spacing) { int row = node.getRow(); int column = node.getColumn(); List<Node> list = getColumn(column).getNodeList(); for (int i=row; i<list.size(); i++) { Node n = list.get(i); shiftNode(n, dy); } Node parent = node.getParent(); shiftParent(parent, spacing); } private void shiftParent(Node parent, int spacing) { if (parent == null) return; int row = parent.getRow(); int column = parent.getColumn(); List<Node> list = getColumn(column).getNodeList(); for (int i=row; i<list.size(); i++) { Node n = list.get(i); placeParent(n, spacing); } shiftParent(parent.getParent(), spacing); } private void placeParent(Node n, int spacing) { // System.out.println("placeParent " + n.getTypeText()); List<Node> kids = n.getChildren(); if (kids == null) { int dy = getDeltaY(n, spacing); if (dy > 0) { int top = n.getTop() + dy; n.setTop(top); } return; } Node firstChild = kids.get(0); Node lastChild = kids.get(kids.size()-1); int kidsTop = firstChild.getTop(); int kidsBottom = lastChild.getBottom(); int middle = (kidsTop + kidsBottom)/2; int dy = n.getHeight()/2; int top = middle - dy; n.setTop(top); } /** * Shift the given node, and all of its children recursively by the given vertical increment. */ private void shiftNode(Node node, int dy) { int top = node.getBoundary().getY() + dy; node.setTop(top); if (node.getChildren() != null) { for (Node c : node.getChildren()) { shiftNode(c, dy); } } } public void add(Node node, int column) { while (columnList.size() < column+1) { columnList.add(new Column()); } Column c = columnList.get(column); int row = c.getNodeList().size(); c.add(node); node.setGridCoordinates(row, column); } public void computeVerticalSpacing(int verticalSpacing) { computeDefaultVerticalSpacing(verticalSpacing); } private void computeDefaultVerticalSpacing(int spacing) { doBaselineVerticalSpacing(spacing); doRootVerticalSpacing(spacing); } /** * Aligns the root node vertically so that it is centered in the middle of its child nodes. */ private void doRootVerticalSpacing(int spacing) { if (columnList.size()<2) return; Node root = getNode(0, 0); int height = columnList.get(1).getBoundary().getHeight(); int middle = height/2; int dy = root.getBoundary().getHeight()/2; int top = middle - dy; root.setTop(top); } private void doBaselineVerticalSpacing(int spacing) { if (columnList.size()==1) { columnList.get(0).getNodeList().get(0).setTop(0); return; } Column col = columnList.get(1); int mark = -spacing; for (Node n : col.getNodeList()) { int top = mark + spacing; n.setTop(top); Rect boundary = n.getBoundary(); mark = top + boundary.getHeight(); doChildrenBaseline(n, spacing); } col.getBoundary().setHeight(mark); } /** * Aligns child nodes so that they are evenly spaced and centered on the middle of the the parent. */ private void doChildrenBaseline(Node parent, int spacing) { List<Node> kids = parent.getChildren(); if (kids == null) return; Rect parentBoundary = parent.getBoundary(); int boxHeight = parentBoundary.getHeight(); int kidCount = kids.size(); int totalKidHeight = kidCount*boxHeight + (kidCount-1) * spacing; int parentMiddle = parentBoundary.getY() + boxHeight/2; int top = parentMiddle - totalKidHeight/2; for (Node child : kids) { child.setTop(top); doChildrenBaseline(child, spacing); top += spacing + boxHeight; } } public void computeHorizontalSpacing(int horizontalSpacing) { computeColumnWidths(); setColumnLeftEdges(horizontalSpacing); } private void setColumnLeftEdges(int spacing) { for (int i=1; i<columnList.size(); i++) { Column col = columnList.get(i); Column prev = columnList.get(i-1); Rect prevBoundary = prev.getBoundary(); int left = prevBoundary.getX() + prevBoundary.getWidth() + spacing; col.setLeft(left); } } private void computeColumnWidths() { for (Column col : columnList) { computeWidth(col); } } private void computeWidth(Column col) { List<Node> nodeList = col.getNodeList(); int maxWidth = 0; for (Node n : nodeList) { Rect boundary = n.getBoundary(); if (boundary.getWidth() > maxWidth) { maxWidth = boundary.getWidth(); } } col.getBoundary().setWidth(maxWidth); } void inject(Node root) { inject(0, root); } private void inject(int column, Node node) { add(node, column); List<Node> kids = node.getChildren(); if (kids != null) { column++; for (Node n : kids) { inject(column, n); } } } } }