/**
* Copyright (C) 2008-2010 Daniel Senff
*
* 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package de.danielsenff.imageflow.gui;
import ij.IJ;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import org.jdesktop.application.Application;
import org.jdesktop.application.ResourceMap;
import org.jfree.text.TextBlock;
import org.jfree.text.TextBlockAnchor;
import org.jfree.text.TextUtilities;
import org.jfree.ui.HorizontalAlignment;
import visualap.GPanel;
import visualap.GPanelListener;
import visualap.Node;
import de.danielsenff.imageflow.controller.DelegatesController;
import de.danielsenff.imageflow.controller.GraphController;
import de.danielsenff.imageflow.models.NodeList;
import de.danielsenff.imageflow.models.SelectionList;
import de.danielsenff.imageflow.models.connection.Connection;
import de.danielsenff.imageflow.models.connection.Input;
import de.danielsenff.imageflow.models.connection.Output;
import de.danielsenff.imageflow.models.connection.Pin;
import de.danielsenff.imageflow.models.delegates.Delegate;
import de.danielsenff.imageflow.models.delegates.UnitDescription;
import de.danielsenff.imageflow.models.unit.CommentNode;
import de.danielsenff.imageflow.models.unit.SourceUnitElement;
import de.danielsenff.imageflow.models.unit.UnitElement;
import de.danielsenff.imageflow.models.unit.UnitFactory;
import de.danielsenff.imageflow.models.unit.UnitList;
/**
* Graphical workspace on which the units are drawn and which handles the mouse actions.
* @author danielsenff
*
*/
public class GraphPanel extends GPanel {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* Draw a small grid on the {@link GraphPanel}
*/
protected boolean drawGrid = false;
/**
* size of the grid
*/
public static int GRIDSIZE = 30;
/**
* Auto align nodes
*/
protected boolean align = false;
/**
* Draw the introduction and help on the empty workspace
*/
private WelcomeArea welcomeArea;
/**
* @param panelListener
* @param graphController
*/
public GraphPanel(final GPanelListener panelListener, final GraphController graphController) {
super(panelListener);
setGraphController(graphController);
this.welcomeArea = new WelcomeArea();
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
if(!IJ.isMacintosh())
this.setBorder(BorderFactory.createLoweredBevelBorder());
this.setDropTarget(new DropTarget(this, new GraphPanelDropHandler(this, graphController)));
}
@Override
protected void paintPrintableConnection(final Graphics g, final Connection connection) {
final Point from = connection.getInput().getOrigin();
final Point to = connection.getOutput().getOrigin();
g.setColor( (connection.isCompatible()) ? Color.BLACK : Color.RED );
g.drawLine(from.x, from.y, to.x, to.y);
if(connection.isLocked()) {
final int dX = Math.abs(from.x - to.x)/2 + Math.min(from.x, to.x);
final int dY = Math.abs(from.y - to.y)/2 + Math.min(from.y, to.y);
final Point origin = new Point(dX, dY);
g.setColor(Color.BLACK);
final Ellipse2D.Double circle =
new Ellipse2D.Double(origin.getX()-5, origin.getY()-5, 10, 10);
((Graphics2D)g).draw(circle);
g.fillRect(origin.x-5, origin.y, 10, 10);
}
if(!connection.isCompatible()) {
final int dX = Math.abs(from.x - to.x)/2 + Math.min(from.x, to.x);
final int dY = Math.abs(from.y - to.y)/2 + Math.min(from.y, to.y);
final Point origin = new Point(dX, dY);
drawErrorMessage((Graphics2D) g, "incompatible data type", origin);
}
}
/* (non-Javadoc)
* @see visualap.GPanel#paintComponent(java.awt.Graphics)
*/
@Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
final Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
if(!getUnitList().isEmpty()) {
//paint grid
if(drawGrid)
paintGrid(g2);
// paint printable items
paintPrintable(g2);
// paint non printable items
if (drawEdge != null) {
// drag new connection
int margin = 18;
for (final Node node : nodeL) {
// if node is a UnitElement and not trying to connect to itself
if (node instanceof UnitElement && (!drawEdge.getParent().equals(node)) ) {
// get units margin, could be lesser than 18 if unit has many pins
margin = ((UnitElement) node).getPinTolerance();
// check if mouse is within this dimensions of a node#
if(isWithin2DRange(mouse, node.getOrigin(), node.getDimension(), margin)) {
// draw every pin
for (final Pin pin : ((UnitElement)node).getInputs()) {
drawCompatbilityIndicator(g2, margin, pin);
}
for (final Pin pin : ((UnitElement)node).getOutputs()) {
drawCompatbilityIndicator(g2, margin, pin);
}
}
}
}
final Point origin = drawEdge.getOrigin();
g2.setColor(Color.BLACK);
// g2.drawLine(origin.x, origin.y, mouse.x, mouse.y);
g2.draw(new Line2D.Double(origin.x, origin.y, mouse.x, mouse.y));
}
//If currentRect exists, paint a box on top.
if (currentRect != null) {
//Draw a rectangle on top of the image.
//depending on image colors
g2.setColor(new Color(0,0,255, 80));
// g2.setStroke(dashed);
g2.setStroke(new BasicStroke(1f));
g2.drawRect(rectToDraw.x, rectToDraw.y,
rectToDraw.width - 1, rectToDraw.height - 1);
g2.setColor(new Color(0,0,255, 40));
g2.fillRect(rectToDraw.x, rectToDraw.y,
rectToDraw.width - 1, rectToDraw.height - 1);
}
} else {
//draw a nice message to promote creating a graph
welcomeArea.draw(g2);
}
}
/**
* @return the align
*/
public boolean isAlign() {
return align;
}
/**
* @param align the align to set
*/
public void setAlign(final boolean align) {
this.align = align;
if(align) {
alignElements();
}
}
/**
* aligns the elements to the grid
*/
public void alignElements() {
for (final Node node : getUnitList()) {
alignElement(node);
}
}
/**
* @param node
*/
private void alignElement(final Node node) {
final Point origin = node.getOrigin();
int x = origin.x;
int y = origin.y;
final int row = y / GRIDSIZE;
final int column = x / GRIDSIZE;
x = column * GRIDSIZE + 10;
y = row * GRIDSIZE + 10;
origin.x = x;
origin.y = y;
}
/**
* @return
*/
protected Collection<Node> getUnitList() {
return this.nodeL;
}
/**
*
* @return
*/
public ResourceMap getResourceMap() {
return Application.getInstance().getContext().getResourceMap(GraphPanel.class);
}
/**
* paints a simple grid on the canvas
* @param g
*/
private void paintGrid(final Graphics g) {
g.setColor(new Color(240, 240, 240));
for (int x = 0; x < this.getWidth(); x+=GRIDSIZE) {
g.drawLine(x, 0, x, getHeight());
}
for (int y = 0; y < this.getHeight(); y+=GRIDSIZE) {
g.drawLine(0, y, getWidth(), y);
}
}
private void drawCompatbilityIndicator(final Graphics2D g2, final int margin, final Pin pin) {
final int diameter = 15;
final Point pinLocation = pin.getOrigin();
final int pinX = pinLocation.x - (diameter/2);
final int pinY = pinLocation.y - (diameter/2);
// draw pin marker if mouse within inner range
if(isWithin2DRange(mouse, pin.getOrigin(), new Dimension(0,0), margin)) {
boolean isCompatible = false;
boolean isLoop = false;
boolean isLocked = false;
if( !(drawEdge instanceof Output && pin instanceof Output)
&& !(drawEdge instanceof Input && pin instanceof Input)) {
// we don't know, if the connection is created
// input first or output first
// isCompatible = drawEdge.isCompatible(pin);
isCompatible = pin.isCompatible(drawEdge);
if(drawEdge instanceof Output && pin instanceof Input) {
isLoop = ((Input)pin).isConnectedInOutputBranch(drawEdge.getParent());
}
if (drawEdge instanceof Input && pin instanceof Output) {
isLoop = ((Output)pin).existsInInputSubgraph(drawEdge.getParent());
}
if(drawEdge.isLocked() || pin.isLocked() ) {
isLocked = true;
}
g2.setColor((isCompatible && !isLoop) ? Color.green : Color.red);
final Ellipse2D.Double circle =
new Ellipse2D.Double(pinX, pinY, diameter, diameter);
g2.fill(circle);
g2.setColor(new Color(0,0,0,44));
g2.draw(circle);
String errorMessage = "";
if(!isCompatible)
errorMessage += "Incompatible data type \n";
if(isLoop)
errorMessage += "Loops are not allowed\n";
if(isLocked)
errorMessage += "Pin is locked\n";
if(errorMessage.length() != 0)
drawErrorMessage(g2, errorMessage, pin.getOrigin());
}
}
}
private void drawErrorMessage(final Graphics2D g2, final String text, final Point origin) {
final Vector<String> lines = tokenizeString(text, "\n");
g2.setColor(Color.RED);
final Ellipse2D.Double circle =
new Ellipse2D.Double(origin.getX()-3, origin.getY()-3, 5, 5);
g2.fill(circle);
g2.setFont(new Font("Arial", Font.PLAIN, 12));
final FontMetrics fm = g2.getFontMetrics();
final Dimension dimension = new Dimension();
String longestLine = "";
for (final String line : lines) {
if(line.length() > longestLine.length())
longestLine = line;
}
dimension.setSize(fm.stringWidth(longestLine) + 10, (fm.getHeight() + 4)*lines.size());
final int padding = 2;
g2.setColor(new Color(255,255,180));
g2.fillRoundRect(origin.x-padding, origin.y-padding,
dimension.width+padding, dimension.height+padding, 4, 4);
g2.setStroke(new BasicStroke(1f));
g2.setColor(new Color(255,0,0,44));
g2.drawRoundRect(origin.x-padding, origin.y-padding,
dimension.width+padding, dimension.height+padding, 4, 4);
g2.setColor(Color.BLACK);
final int lineheight = fm.getHeight() + 5;
int yT = (origin.y + padding) + fm.getAscent();
for (final String line : lines) {
g2.drawString(line, origin.x + 5, yT);
yT += lineheight;
}
}
private Vector<String> tokenizeString(final String text, final String token) {
final StringTokenizer stringTokenizer = new StringTokenizer(text, token);
final Vector<String> lines = new Vector<String>();
while(stringTokenizer.hasMoreTokens()) {
lines.add(stringTokenizer.nextToken());
}
return lines;
}
/**
* Returns true if the value is within this range of values.
* Excludes the limits.
* @param compareValue
* @param startValue
* @param endValue
* @return
*/
public static boolean isWithinRange(final int compareValue,
final int startValue,
final int endValue) {
return (compareValue > startValue) && (compareValue < endValue);
}
/**
* Returns true, if the current point is within this 2D-range.
* The Range is defined by its coordinates,
* the dimension of the rectangle and a margin around this rectangle.
* @param currentPoint
* @param origin
* @param dimension
* @param margin
* @return
*/
public static boolean isWithin2DRange(final Point currentPoint,
final Point origin,
final Dimension dimension,
final int margin) {
return isWithinRange(currentPoint.x, origin.x-margin,
origin.x+dimension.width+margin)
&& isWithinRange(currentPoint.y, origin.y - margin,
origin.y + dimension.height + margin);
}
@Override
public void mouseReleased(final MouseEvent e) {
super.mouseReleased(e);
if(align)
alignElements();
this.requestFocusInWindow(true);
}
/**
* Replace the current {@link UnitList} with a different one.
* @param units
*/
@Override
public void setNodeL(final NodeList<Node> units) {
super.nodeL = units;
}
/**
* If true, a grid is drawn on the workspace
* @return
*/
public boolean isDrawGrid() {
return drawGrid;
}
/**
* (De)activate the grid on the workspace.
* @param drawGrid
*/
public void setDrawGrid(final boolean drawGrid) {
this.drawGrid = drawGrid;
}
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() > 1) {
if (selection.size() == 1) properties(selection.get(0), e.getLocationOnScreen());
else selection.clear(); //zz to be handled in more completed way
}
}
public void properties(final Node node, Point point) {
if (node instanceof CommentNode) {
final String inputValue = JOptionPane.showInputDialog("Edit text:",((CommentNode)node).getText());
if ((inputValue != null) && (inputValue.length() != 0)) {
((CommentNode)node).setText(inputValue);
repaint();
}
}
else {
final UnitElement unit = (UnitElement) node;
unit.showProperties(point);
}
}
/**
*
* @param graphController
*/
public void setGraphController(final GraphController graphController) {
this.selection.clear();
this.nodeL = graphController.getUnitElements();
this.connectionList = graphController.getConnections();
this.selection = graphController.getSelections();
invalidate();
}
/**
*
* @param selections
*/
public void setSelections(final SelectionList selections) {
this.selection = selections;
}
}