/*******************************************************************************
* 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.graphics;
import java.awt.Color;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.List;
/**
* A widget that can have connectors attached to it.
* @author Greg McFall
*
*/
public class ConnectableWidget extends BaseRect implements HasConnectors, Widget {
private Style style;
private ArcList leftArcs;
private ArcList rightArcs;
private ArcList topArcs;
private ArcList bottomArcs;
private Widget body;
private Widget parent;
private int routeSpacing = 10;
public ConnectableWidget(Widget body, Style style) {
this.style = style;
this.body = body;
body.setParent(this);
}
public Widget getBody() {
return body;
}
@Override
public List<ArcEnd> listLeftArcs() {
return leftArcs;
}
@Override
public List<ArcEnd> listTopArcs() {
return topArcs;
}
@Override
public List<ArcEnd> listRightArcs() {
return rightArcs;
}
@Override
public List<ArcEnd> listBottomArcs() {
return bottomArcs;
}
@Override
public void addLeftArc(ArcEnd end) {
if (leftArcs == null) {
leftArcs = new ArcList();
}
leftArcs.add(end);
end.attachTo(this);
}
@Override
public void addRightArc(ArcEnd end) {
if (rightArcs == null) {
rightArcs = new ArcList();
}
rightArcs.add(end);
end.attachTo(this);
}
@Override
public void addTopArc(ArcEnd end) {
if (topArcs == null) {
topArcs = new ArcList();
}
topArcs.add(end);
end.attachTo(this);
}
@Override
public void addBottomArc(ArcEnd end) {
if (bottomArcs == null) {
bottomArcs = new ArcList();
}
bottomArcs.add(end);
end.attachTo(this);
}
/**
* Layout the body contained within this ConnectableWidget, and also place
* the ArcEnd entities along the edges.
* If the default size of this widget is not big enough to accommodate the
* connectors, then the widget will be enlarged.
*/
@Override
public void layout() {
body.layout();
int bodyWidth = body.getBounds().getWidth();
int bodyHeight = body.getBounds().getHeight();
setWidth(bodyWidth);
setHeight(bodyHeight);
int height = computeArcHeight(rightArcs, bodyWidth);
height = Math.max(height, computeArcHeight(leftArcs, 0));
int width = computeArcWidth(topArcs, 0);
width = Math.max(width, computeArcWidth(bottomArcs, bodyHeight));
if (height > bodyHeight) {
setHeight(height);
}
attachRight();
attachLeft();
attachTop();
attachBottom();
}
private void attachTop() {
if (topArcs==null || topArcs.isEmpty()) return;
attachHorizontal(topArcs, 0);
}
private void attachBottom() {
if (bottomArcs==null || bottomArcs.isEmpty()) return;
attachHorizontal(bottomArcs, getHeight());
}
private void attachLeft() {
if (leftArcs == null || leftArcs.isEmpty()) return;
attachVertical(leftArcs, 0);
//
// ArcList arcList = leftArcs;
//
// int x = 0;
//
// int middle = getHeight()/2;
// int halfSpan = arcList.getHalfSpan();
//
// int maxEndSpan = arcList.getMaxEndSpan();
//
// int y = middle - halfSpan;
//
// int dx = 0;
// int marginBottom = 0;
// int delta = 0;
//
// // The "extraSpace" variable denotes the amount by which
// // we must expand the width of all endpoints to ensure that there is adequate space for labels.
// int extraSpace = 0;
//
// for (ArcEnd end : arcList) {
// int marginTop = end.getStyle().getMarginTop();
// int margin = Math.max(marginBottom, marginTop);
// int ascent = end.getAscent();
// if (delta != 0) {
// y += margin+ascent;
// }
//
//
// end.attachAt(x, y);
// y += end.getDescent();
//
//
// // Compute the width of the end.
// // This will determine where the arc bends.
//
// if (y >= middle && delta>0) {
// delta = 0;
// }
// dx += delta;
//
// int endWidth = maxEndSpan - Math.max(dx, 0);
// int oldBounds = end.getBounds().getWidth();
//
// if (endWidth < oldBounds) {
// extraSpace = Math.max(extraSpace, oldBounds - endWidth);
// }
//
// end.getBounds().setWidth(endWidth);
//
// if (y <= middle) {
// delta = routeSpacing;
//
// } else {
// delta = -routeSpacing;
// }
// }
//
// if (extraSpace > 0) {
// applyExtraSpace(extraSpace, arcList);
// }
}
// private void applyExtraSpace(int extraSpace, ArcList arcList) {
//
// for (ArcEnd end : arcList) {
// int width = end.getBounds().getWidth() + extraSpace;
// end.getBounds().setWidth(width);
// }
//
// }
private void attachRight() {
if (rightArcs == null || rightArcs.isEmpty()) return;
attachVertical(rightArcs, getWidth());
}
public void setEndWidths(List<ArcEnd> arcList) {
setEndWidthsFromTop(arcList);
setEndWidthsFromBottom(arcList);
}
private void setEndWidthsFromBottom(List<ArcEnd> arcList) {
if (arcList == null) return;
int index = arcList.size()-1;
// Work top-down until we find an end point that is higher than it's peer.
int w = 0;
for (; index >= 0; index--) {
ArcEnd e0 = arcList.get(index);
ArcEnd e1 = e0.getArc().getOtherEnd(e0);
Transformer t0 = new Transformer(e0.getAttachedWidget());
Transformer t1 = new Transformer(e1.getAttachedWidget());
int ex0 = e0.getArcX();
int ey0 = e0.getArcY();
int ex1 = e1.getArcX();
int ey1 = e1.getArcY();
int x0 = t0.x(ex0, ey0);
int y0 = t0.y(ex0, ey0);
int x1 = t1.x(ex1, ey1);
int y1 = t1.y(ex1, ey1);
if (x1 < x0) {
int y = y1;
y1 = y0;
y0 = y;
e0 = e1;
}
if (y1 < y0) {
break;
}
w = Math.max(w, e0.getBounds().getWidth());
if (index != arcList.size()-1) {
w += routeSpacing;
}
e0.getBounds().setWidth(w);
}
}
private void setEndWidthsFromTop(List<ArcEnd> arcList) {
if (arcList == null) return;
int index = 0;
// Work top-down until we find an end point that is higher than it's peer.
int w = 0;
for (; index < arcList.size(); index++) {
ArcEnd e0 = arcList.get(index);
ArcEnd e1 = e0.getArc().getOtherEnd(e0);
Transformer t0 = new Transformer(e0.getAttachedWidget());
Transformer t1 = new Transformer(e1.getAttachedWidget());
int ex0 = e0.getArcX();
int ey0 = e0.getArcY();
int ex1 = e1.getArcX();
int ey1 = e1.getArcY();
int x0 = t0.x(ex0, ey0);
int y0 = t0.y(ex0, ey0);
int x1 = t1.x(ex1, ey1);
int y1 = t1.y(ex1, ey1);
if (x1 < x0) {
int y = y1;
y1 = y0;
y0 = y;
e0 = e1;
}
if (y1 >= y0)
break;
w = Math.max(w, e0.getBounds().getWidth());
if (index > 0) {
w += routeSpacing;
}
e0.getBounds().setWidth(w);
}
}
private void attachVertical(ArcList arcList, int x) {
int middle = getHeight()/2;
int halfSpan = arcList.getHalfSpan();
// int maxEndSpan = arcList.getMaxEndSpan();
int y = middle - halfSpan;
// int dx = 0;
int marginBottom = 0;
int delta = 0;
for (ArcEnd end : arcList) {
int marginTop = end.getStyle().getMarginTop();
int margin = Math.max(marginBottom, marginTop);
int ascent = end.getAscent();
if (delta != 0) {
y += margin+ascent;
}
end.attachAt(x, y);
y += end.getDescent();
// Compute the width of the end.
// This will determine where the arc bends.
if (y >= middle && delta>0) {
delta = 0;
}
// dx += delta;
// int endWidth = maxEndSpan + Math.max(dx, 0);
// end.getBounds().setWidth(endWidth);
if (y <= middle) {
delta = routeSpacing;
} else {
delta -= routeSpacing;
}
}
}
private void attachHorizontal(ArcList arcList, int yEdge) {
int center = getWidth()/2;
int halfSpan = arcList.getHalfSpan();
int x = center - halfSpan;
int dy = 0;
int marginRight = 0;
int delta = 0;
for (ArcEnd end : arcList) {
int marginLeft = end.getStyle().getMarginLeft();
int margin = Math.max(marginRight, marginLeft);
int dx = end.getX() - end.getBounds().getLeft();
if (delta != 0) {
x += margin+dx;
}
end.attachAt(x, yEdge);
end.layout();
// Adjust the height of the end, which determines
// where the arc bends.
if (x >= center && delta>0) {
delta = 0;
}
dy += delta;
int endHeight = end.getBounds().getHeight() + dy;
end.getBounds().setHeight(endHeight);
if (x <= center) {
delta = routeSpacing;
} else {
delta -= routeSpacing;
}
}
}
private int computeArcWidth(ArcList arcList, int yEdge) {
if (arcList == null || arcList.isEmpty()) return 0;
int marginRight = 0;
int center = getWidth()/2;
int x = 0; // The marker for our current position along the edge.
int x1 = 0; // The x-coordinate where the first arc attaches.
int x2 = 0; // The x-coordinate where the last arc attaches
for (ArcEnd end : arcList) {
int marginLeft = end.getStyle().getMarginLeft();
int margin = Math.max(marginLeft, marginRight);
// Temporarily attach to the center top of this widget so we can
// compute the bounds of the ArcEnd.
//
end.attachAt(center, yEdge);
end.layout();
int endLeft = end.getBounds().getLeft();
int endRight = end.getBounds().getRight();
int endX = end.getX(); // The point where the arc attaches.
// Compute the distance from the left edge of the ArcEnd's bounding box
// to the point where the arc attaches.
int dx = endX - endLeft;
// This is actually where we want to actually attach the edge.
x2 = x + margin + dx;
if (x1 == 0) {
x1 = x2;
}
x = x2 + endRight - endX;
marginRight = end.getStyle().getMarginRight();
}
int width = x2 - x1;
arcList.setSpan(width);
setHorizontalHalfSpan(arcList);
width += 2*routeSpacing;
return width;
}
private void setHorizontalHalfSpan(ArcList arcList) {
int size = arcList.size();
int span = arcList.getSpan();
int halfSpan = span/2;
boolean even = size%2 == 0;
int x0 = arcList.get(0).getX();
int middle = x0 + halfSpan;
for (int i=0; i<size; i++) {
ArcEnd end = arcList.get(i);
int x = end.getX();
if (x == middle) {
halfSpan = x - x0;
break;
}
if (x > middle) {
if (even) {
halfSpan = (x + arcList.get(i-1).getX())/2 - x0;
} else {
halfSpan = x - x0;
}
break;
}
}
arcList.setHalfSpan(halfSpan);
}
private int computeArcHeight(ArcList arcList, int x) {
if (arcList == null || arcList.isEmpty()) return 0;
int marginBottom = 0;
int y = 0;
int y1 = 0;
int y2 = 0;
int tempY = getTop() + 1;
int maxEndSpan = 0;
for (ArcEnd end : arcList) {
int marginTop = end.getStyle().getMarginTop();
int margin = Math.max(marginBottom, marginTop);
// y2 is the position where the arc end terminates
y2 = y + margin + end.getAscent();
if (y1 == 0) {
y1 = y2;
}
// Make a temporary attachment so that we can compute the width
end.attachAt(x, tempY);
maxEndSpan = Math.max(maxEndSpan, end.getBounds().getWidth());
end.setY(y2);
y = y2 + end.getDescent();
marginBottom = end.getStyle().getMarginBottom();
}
int height = y2 - y1;
arcList.setMaxEndSpan(maxEndSpan);
arcList.setSpan(height);
setVerticalHalfSpan(arcList);
height += 2*routeSpacing;
return height;
}
private void setVerticalHalfSpan(ArcList arcList) {
int size = arcList.size();
int span = arcList.getSpan();
int halfSpan = span/2;
boolean even = size%2 == 0;
int y0 = arcList.get(0).getY();
int middle = y0 + halfSpan;
int titleCount = 0;
for (int i=0; i<size; i++) {
ArcEnd end = arcList.get(i);
if (end.getTitle() != null) {
titleCount++;
}
int y = end.getY();
if (y == middle) {
halfSpan = y - y0;
break;
}
if (y > middle) {
if (even) {
halfSpan = (y + arcList.get(i-1).getY())/2 - y0;
} else {
halfSpan = y - y0;
}
break;
}
}
if (titleCount == 0 || titleCount==size) {
arcList.setHalfSpan(halfSpan);
} else {
arcList.setHalfSpan(span/2);
}
}
@Override
public void setPosition(int left, int top) {
setLeft(left);
setTop(top);
}
/**
* A convenience method for painting a list of arcs attached to this widget
* (or any other widget).
* @param list
*/
public void paintArcs(Graphics2D g, List<ArcEnd> list) {
if (list == null) return;
for (ArcEnd end : list) {
end.getArc().paint(g);
}
}
@Override
public void paint(Graphics2D g) {
int width = getWidth();
int height = getHeight();
Color bgColor = style.getBgColor();
if (bgColor != null) {
g.setColor(bgColor);
g.fillRect(0, 0, width, height);
}
GraphicsUtil.paint(g, body);
Color borderColor = style.getBorderColor();
if (borderColor != null) {
g.setColor(borderColor);
g.drawRect(0, 0, width, height);
}
}
@Override
public Rect getBounds() {
return this;
}
@Override
public Style getStyle() {
return style;
}
public void setStyle(Style style) {
this.style = style;
}
@SuppressWarnings("serial")
static class ArcList extends ArrayList<ArcEnd> {
private int span;
private int halfSpan;
private int maxEndSpan;
public int getSpan() {
return span;
}
public void setSpan(int span) {
this.span = span;
}
public void setHalfSpan(int s) {
halfSpan = s;
}
public int getHalfSpan() {
return halfSpan;
}
public int getMaxEndSpan() {
return maxEndSpan;
}
public void setMaxEndSpan(int maxEndSpan) {
this.maxEndSpan = maxEndSpan;
}
}
@Override
public Widget getParent() {
return parent;
}
@Override
public void setParent(Widget parent) {
this.parent = parent;
}
}