/*
* Copyright 2013 Serdar.
*
* 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 de.fub.maps.project.aggregator.graph;
import de.fub.maps.project.aggregator.pipeline.AbstractAggregationProcess;
import de.fub.utilsmodule.Collections.ObservableArrayList;
import de.fub.utilsmodule.Collections.ObservableList;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.JPopupMenu;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.visual.action.AcceptProvider;
import org.netbeans.api.visual.action.ActionFactory;
import org.netbeans.api.visual.action.ConnectProvider;
import org.netbeans.api.visual.action.ConnectorState;
import org.netbeans.api.visual.action.PopupMenuProvider;
import org.netbeans.api.visual.action.ReconnectProvider;
import org.netbeans.api.visual.action.SelectProvider;
import org.netbeans.api.visual.anchor.Anchor;
import org.netbeans.api.visual.anchor.AnchorFactory;
import org.netbeans.api.visual.anchor.AnchorShape;
import org.netbeans.api.visual.anchor.PointShape;
import org.netbeans.api.visual.graph.GraphScene;
import org.netbeans.api.visual.router.RouterFactory;
import org.netbeans.api.visual.widget.ConnectionWidget;
import org.netbeans.api.visual.widget.LayerWidget;
import org.netbeans.api.visual.widget.Scene;
import org.netbeans.api.visual.widget.Widget;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Children;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.util.ChangeSupport;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.WeakListeners;
/**
* TODO besseren layouter implementieren.
*
* @author Serdar
*/
public class ProcessGraph extends GraphScene<AbstractAggregationProcess<?, ?>, String> implements ExplorerManager.Provider {
private static long edgeCount = 0;
private LayerWidget backgroundLayer = null;
private LayerWidget mainLayer = null;
private LayerWidget connectionLayer = null;
private LayerWidget interactionLayer = null;
private final ChangeSupport pcs = new ChangeSupport(this);
private final HashMap<String, Integer> javaTypeCounterMap = new HashMap<String, Integer>();
private final ExplorerManager explorerManager = new ExplorerManager();
private final Lookup lookup = ExplorerUtils.createLookup(explorerManager, new ActionMap());
private final ObservableList<AbstractAggregationProcess<?, ?>> processList = new ObservableArrayList<AbstractAggregationProcess<?, ?>>();
private final NodeFactory nodeFactory;
public ProcessGraph() {
nodeFactory = new NodeFactory();
explorerManager.setRootContext(new AbstractNode(Children.create(nodeFactory, true)));
getActions().addAction(ActionFactory.createZoomAction(1.5, true));
backgroundLayer = new LayerWidget(ProcessGraph.this);
mainLayer = new LayerWidget(ProcessGraph.this);
connectionLayer = new LayerWidget(ProcessGraph.this);
interactionLayer = new LayerWidget(ProcessGraph.this);
addChild(backgroundLayer);
addChild(mainLayer);
addChild(connectionLayer);
addChild(interactionLayer);
getActions().addAction(ActionFactory.createAcceptAction(new AcceptProviderImpl()));
}
@Override
public Lookup getLookup() {
return lookup;
}
public synchronized String createEdge() {
return MessageFormat.format("edge-{0}", edgeCount++);
}
public void addChangeListener(ChangeListener listener) {
pcs.addChangeListener(listener);
}
public void removeChangeListener(ChangeListener listener) {
pcs.removeChangeListener(listener);
}
public void clearGraph() {
nodeFactory.objectToNodeMap.clear();
processList.clear();
javaTypeCounterMap.clear();
Collection<AbstractAggregationProcess<?, ?>> nodes = getNodes();
ArrayList<AbstractAggregationProcess<?, ?>> list = new ArrayList<AbstractAggregationProcess<?, ?>>(nodes);
for (AbstractAggregationProcess<?, ?> process : list) {
removeNodeWithEdges(process);
validate();
}
mainLayer.removeChildren();
validate();
connectionLayer.removeChildren();
validate();
interactionLayer.removeChildren();
validate();
}
private Integer getTypeCounter(AbstractAggregationProcess<?, ?> process) {
if (!javaTypeCounterMap.containsKey(process.getProcessDescriptor().getJavaType())) {
javaTypeCounterMap.put(process.getProcessDescriptor().getJavaType(), new Integer(1));
}
return javaTypeCounterMap.get(process.getProcessDescriptor().getJavaType());
}
private void updateTypeCounter(AbstractAggregationProcess<?, ?> process) {
Integer typeCounter = getTypeCounter(process);
javaTypeCounterMap.put(process.getProcessDescriptor().getJavaType(), ++typeCounter);
Logger.getLogger(ProcessGraph.class.getName()).log(Level.INFO, MessageFormat.format("{0} count: {1}", process.getProcessDescriptor().getJavaType(), typeCounter));
}
@Override
protected Widget attachNodeWidget(AbstractAggregationProcess<?, ?> process) {
updateAggregatorPipeline();
processList.add(process);
ProcessWidget processWidget = new ProcessWidget(this, process);
processWidget.setPreferredLocation(new Point(0, 0));
Widget widget = new Widget(getScene());
widget.setBackground(Color.BLACK);
widget.getActions().addAction(ActionFactory.createMoveAction());
processWidget.getActions().addAction(ActionFactory.createExtendedConnectAction(interactionLayer, new SceneConnectProvider()));
widget.getActions().addAction(ActionFactory.createSelectAction(new ProcessSelectProvider()));
widget.addChild(processWidget);
widget.setPreferredSize(processWidget.getPreferredSize());
mainLayer.addChild(widget);
mainLayer.revalidate();
validate();
return widget;
}
@Override
protected Widget attachEdgeWidget(String edge) {
ConnectionWidget connectionWidget = new ConnectionWidget(this);
connectionWidget.setTargetAnchorShape(AnchorShape.TRIANGLE_FILLED);
connectionWidget.setEndPointShape(PointShape.SQUARE_FILLED_BIG);
connectionWidget.setRouter(RouterFactory.createOrthogonalSearchRouter(mainLayer));
connectionWidget.getActions().addAction(ActionFactory.createReconnectAction(new SceneReconnectProvider()));
connectionWidget.getActions().addAction(createObjectHoverAction());
connectionWidget.getActions().addAction(createSelectAction());
connectionWidget.getActions().addAction(ActionFactory.createPopupMenuAction(new ConnectionPopUp()));
connectionLayer.addChild(connectionWidget);
connectionLayer.revalidate();
validate();
return connectionWidget;
}
@Override
protected void attachEdgeSourceAnchor(String edge, AbstractAggregationProcess<?, ?> oldSourceNode, AbstractAggregationProcess<?, ?> sourceNode) {
Widget sourceNodeWidget = findWidget(sourceNode);
Anchor sourceAnchor = AnchorFactory.createRectangularAnchor(sourceNodeWidget);
ConnectionWidget edgeWidget = (ConnectionWidget) findWidget(edge);
edgeWidget.setSourceAnchor(sourceAnchor);
validate();
}
@Override
protected void attachEdgeTargetAnchor(String edge, AbstractAggregationProcess<?, ?> oldTargetNode, AbstractAggregationProcess<?, ?> targetNode) {
Widget targetNodeWidget = findWidget(targetNode);
Anchor targetAnchor = AnchorFactory.createRectangularAnchor(targetNodeWidget);
ConnectionWidget edgeWidget = (ConnectionWidget) findWidget(edge);
edgeWidget.setTargetAnchor(targetAnchor);
validate();
}
private Class<?> getInputType(AbstractAggregationProcess target) {
Class<?> targetInputType = null;
List<Class<?>> targetInputTypes = getInputTypes(target);
if (!targetInputTypes.isEmpty()) {
if (targetInputTypes.size() == 2) {
for (Class<?> c : targetInputTypes) {
if (!c.equals(Object.class)) {
targetInputType = c;
break;
}
}
} else {
targetInputType = targetInputTypes.iterator().next();
}
}
return targetInputType;
}
private List<Class<?>> getInputTypes(AbstractAggregationProcess target) {
ArrayList<Class<?>> resultList = new ArrayList<Class<?>>();
Class<? extends AbstractAggregationProcess> aClass = target.getClass();
Method[] declaredMethods = aClass.getDeclaredMethods();
for (Method method : declaredMethods) {
if ("setInput".equals(method.getName())) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {
resultList.add(parameterTypes[0]);
}
}
}
return resultList;
}
private Class<?> getOutputType(AbstractAggregationProcess source) {
Class<?> sourceOutputType = null;
List<Class<?>> sourceOutputTypes = getOutputTypes(source);
if (!sourceOutputTypes.isEmpty()) {
if (sourceOutputTypes.size() == 2) {
for (Class<?> c : sourceOutputTypes) {
if (!c.equals(Object.class)) {
sourceOutputType = c;
break;
}
}
} else {
sourceOutputType = sourceOutputTypes.iterator().next();
}
}
return sourceOutputType;
}
private List<Class<?>> getOutputTypes(AbstractAggregationProcess source) {
ArrayList<Class<?>> resultList = new ArrayList<Class<?>>();
Class<? extends AbstractAggregationProcess> aClass = source.getClass();
Method[] declaredMethods = aClass.getDeclaredMethods();
for (Method method : declaredMethods) {
if ("getResult".equals(method.getName())) {
resultList.add(method.getReturnType());
}
}
return resultList;
}
void updateAggregatorPipeline() {
pcs.fireChange();
}
public List<AbstractAggregationProcess<?, ?>> collectPipeline() {
List<AbstractAggregationProcess<?, ?>> pipeLine = new ArrayList<AbstractAggregationProcess<?, ?>>();
Collection<AbstractAggregationProcess<?, ?>> nodes = getNodes();
for (AbstractAggregationProcess<?, ?> process : nodes) {
Class<?> inputType = getInputType(process);
if (inputType != null && inputType.isAssignableFrom(Void.class)) {
pipeLine = collectPipelineUnits(process);
break;
}
}
return pipeLine;
}
private List<AbstractAggregationProcess<?, ?>> collectPipelineUnits(AbstractAggregationProcess<?, ?> process) {
return collectPipelineUnits(process, new ArrayList<AbstractAggregationProcess<?, ?>>());
}
private List<AbstractAggregationProcess<?, ?>> collectPipelineUnits(AbstractAggregationProcess<?, ?> process, List<AbstractAggregationProcess<?, ?>> list) {
list.add(process);
Collection<String> findNodeEdges = findNodeEdges(process, true, false);
if (findNodeEdges.size() == 1) {
String edge = findNodeEdges.iterator().next();
AbstractAggregationProcess<?, ?> edgeTarget = getEdgeTarget(edge);
if (edgeTarget != null) {
collectPipelineUnits(edgeTarget, list);
}
}
return list;
}
@Override
public ExplorerManager getExplorerManager() {
return explorerManager;
}
private class ProcessSelectProvider implements SelectProvider {
@Override
public boolean isAimingAllowed(Widget widget, Point localLocation, boolean invertSelection) {
return true;
}
@Override
public boolean isSelectionAllowed(Widget widget, Point localLocation, boolean invertSelection) {
return true;
}
@Override
public void select(Widget widget, Point localLocation, boolean invertSelection) {
widget.bringToFront();
widget.setState(widget.getState().deriveSelected(!widget.getState().isSelected()));
widget.getScene().validate();
Node[] nodes = new Node[0];
if (widget.getState().isSelected()) {
Object object = ProcessGraph.this.findObject(widget);
if (object instanceof AbstractAggregationProcess) {
AbstractAggregationProcess process = (AbstractAggregationProcess) object;
if (nodeFactory.objectToNodeMap.containsKey(process)) {
nodes = new Node[]{nodeFactory.objectToNodeMap.get(process)};
}
}
}
try {
getExplorerManager().setSelectedNodes(nodes);
} catch (PropertyVetoException ex) {
Exceptions.printStackTrace(ex);
}
}
}
private class SceneConnectProvider implements ConnectProvider {
@Override
public boolean isSourceWidget(Widget sourceWidget) {
Object object = ProcessGraph.this.findObject(sourceWidget);
AbstractAggregationProcess source = ProcessGraph.this.isNode(object) ? (AbstractAggregationProcess) object : null;
return source != null && ProcessGraph.this.findNodeEdges(source, true, false).isEmpty();
}
@Override
public ConnectorState isTargetWidget(Widget sourceWidget, Widget targetWidget) {
Object sourceObject = ProcessGraph.this.findObject(sourceWidget);
Object targetObject = ProcessGraph.this.findObject(targetWidget);
AbstractAggregationProcess target = ProcessGraph.this.isNode(targetObject) ? (AbstractAggregationProcess) targetObject : null;
AbstractAggregationProcess source = ProcessGraph.this.isNode(sourceObject) ? (AbstractAggregationProcess) sourceObject : null;
// exclude the case source node connects to itself
if (target != null
&& source != null
&& findNodeEdges(target, false, true).isEmpty()
&& !source.equals(target)) {
Class<?> targetInputType = getInputType(target);
if (targetInputType != null && !targetInputType.equals(Void.TYPE)) {
Class<?> sourceOutputType = getOutputType(source);
// check whether source output type and
// target input type are assignable
if (sourceOutputType != null
&& targetInputType.isAssignableFrom(sourceOutputType)) {
return ConnectorState.ACCEPT;
}
}
}
// TODO show message dialog why not accepted.
return ConnectorState.REJECT;
}
@Override
public boolean hasCustomTargetWidgetResolver(Scene scene) {
return false;
}
@Override
public Widget resolveTargetWidget(Scene scene, Point sceneLocation) {
return null;
}
@Override
public void createConnection(Widget sourceWidget, Widget targetWidget) {
String edgeId = MessageFormat.format("edge + {0}", edgeCount++);
ProcessGraph.this.addEdge(edgeId);
Object sourceObject = ProcessGraph.this.findObject(sourceWidget);
Object targetObject = ProcessGraph.this.findObject(targetWidget);
if (ProcessGraph.this.isNode(targetObject)
&& ProcessGraph.this.isNode(sourceObject)
&& sourceObject instanceof AbstractAggregationProcess
&& targetObject instanceof AbstractAggregationProcess) {
setEdgeSource(edgeId, (AbstractAggregationProcess) sourceObject);
setEdgeTarget(edgeId, (AbstractAggregationProcess) targetObject);
updateAggregatorPipeline();
}
validate();
}
}
private class SceneReconnectProvider implements ReconnectProvider {
String edge;
AbstractAggregationProcess<?, ?> originalNode;
AbstractAggregationProcess<?, ?> replacementNode;
@Override
public void reconnectingStarted(ConnectionWidget connectionWidget, boolean reconnectingSource) {
}
@Override
public void reconnectingFinished(ConnectionWidget connectionWidget, boolean reconnectingSource) {
validate();
}
@Override
public boolean isSourceReconnectable(ConnectionWidget connectionWidget) {
Object object = findObject(connectionWidget);
edge = isEdge(object) ? (String) object : null;
originalNode = edge != null ? ProcessGraph.this.getEdgeSource(edge) : null;
return originalNode != null;
}
@Override
public boolean isTargetReconnectable(ConnectionWidget connectionWidget) {
// TODO differen logic for the condition to reconnect to given target
Object object = findObject(connectionWidget);
edge = isEdge(object) ? (String) object : null;
originalNode = edge != null ? getEdgeTarget(edge) : null;
return originalNode != null;
}
@Override
public ConnectorState isReplacementWidget(ConnectionWidget connectionWidget, Widget replacementWidget, boolean reconnectingSource) {
// TODO differen logic for the condition to reconnect to given target
Object object = findObject(replacementWidget);
replacementNode = isNode(object) ? (AbstractAggregationProcess<?, ?>) object : null;
if (replacementNode != null
&& findNodeEdges(replacementNode, false, true).isEmpty()) {
return !originalNode.equals(replacementNode) ? ConnectorState.ACCEPT : ConnectorState.REJECT_AND_STOP;
}
return object != null ? ConnectorState.REJECT_AND_STOP : ConnectorState.REJECT;
}
@Override
public boolean hasCustomReplacementWidgetResolver(Scene scene) {
return false;
}
@Override
public Widget resolveReplacementWidget(Scene scene, Point sceneLocation) {
return null;
}
@Override
public void reconnect(ConnectionWidget connectionWidget, Widget replacementWidget, boolean reconnectingSource) {
// TODO differen logic for the condition to reconnect to given target
if (replacementWidget == null && isEdge(edge)) {
removeEdge(edge);
} else if (reconnectingSource) {
setEdgeSource(edge, replacementNode);
} else {
Object edgeObject = findObject(connectionWidget);
Object nodeObject = findObject(replacementWidget);
if (isEdge(edgeObject) && edgeObject instanceof String
&& isNode(nodeObject) && nodeObject instanceof AbstractAggregationProcess<?, ?>) {
setEdgeTarget((String) edgeObject, (AbstractAggregationProcess<?, ?>) nodeObject);
}
//
}
validate();
updateAggregatorPipeline();
}
}
private class ConnectionPopUp implements PopupMenuProvider {
private JPopupMenu popup = new JPopupMenu();
private Widget owner = null;
public ConnectionPopUp() {
init();
}
private void init() {
popup.add(new AbstractAction("Remove") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
if (owner != null) {
Object findObject = findObject(owner);
if (findObject instanceof String) {
removeEdge((String) findObject);
updateAggregatorPipeline();
}
}
}
});
}
@Override
public JPopupMenu getPopupMenu(Widget widget, Point localLocation) {
owner = widget;
return popup;
}
}
/**
* Handles Drop down event to the graph scene.
*/
private class AcceptProviderImpl implements AcceptProvider {
public AcceptProviderImpl() {
}
@Override
public ConnectorState isAcceptable(Widget widget, Point point, Transferable transferable) {
Widget child = null;
try {
Object transferData = transferable.getTransferData(AbstractAggregationProcess.PROCESS_FLAVOR);
if (transferData instanceof AbstractAggregationProcess<?, ?>) {
AbstractAggregationProcess<?, ?> proc = (AbstractAggregationProcess<?, ?>) transferData;
Class<?> inputType = getInputType(proc);
if (inputType != null) {
if (inputType.isAssignableFrom(Void.class)) {
Collection<AbstractAggregationProcess<?, ?>> nodes = getNodes();
for (AbstractAggregationProcess<?, ?> p : nodes) {
inputType = getInputType(p);
if (inputType.isAssignableFrom(Void.class)) {
return ConnectorState.REJECT;
}
}
// TODO message dialog why didn't get accepted!
return ConnectorState.ACCEPT;
} else {
return ConnectorState.ACCEPT;
}
}
child = findWidget(transferData);
if (child != null) {
return ConnectorState.REJECT;
}
}
} catch (UnsupportedFlavorException ex) {
Exceptions.printStackTrace(ex);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return ConnectorState.ACCEPT;
}
@Override
public void accept(Widget widget, Point point, Transferable transferable) {
try {
Object transferData = transferable.getTransferData(AbstractAggregationProcess.PROCESS_FLAVOR);
if (transferData instanceof de.fub.maps.project.api.process.Process) {
AbstractAggregationProcess process = (AbstractAggregationProcess) transferData.getClass().newInstance();
Integer typeCounter = getTypeCounter(process);
process.getProcessDescriptor().setDisplayName(MessageFormat.format("{0} ({1})", process.getProcessDescriptor().getDisplayName(), typeCounter));
Widget processWidget = addNode(process);
Dimension preferredSize = processWidget.getPreferredSize();
Point point1 = new Point(point.x - preferredSize.width / 2, point.y - preferredSize.height / 2);
Point convertLocalToScene = widget.convertLocalToScene(point1);
processWidget.setPreferredLocation(convertLocalToScene);
validate();
repaint();
updateTypeCounter(process);
}
} catch (InstantiationException ex) {
Exceptions.printStackTrace(ex);
} catch (IllegalAccessException ex) {
Exceptions.printStackTrace(ex);
} catch (UnsupportedFlavorException ex) {
Exceptions.printStackTrace(ex);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
}
private class NodeFactory extends ChildFactory<AbstractAggregationProcess<?, ?>> implements ChangeListener {
private final HashMap<AbstractAggregationProcess<?, ?>, Node> objectToNodeMap = new HashMap<AbstractAggregationProcess<?, ?>, Node>();
public NodeFactory() {
processList.addChangeListener(WeakListeners.change(NodeFactory.this, processList));
}
@Override
protected boolean createKeys(List<AbstractAggregationProcess<?, ?>> toPopulate) {
toPopulate.addAll(processList);
return true;
}
@Override
protected Node createNodeForKey(AbstractAggregationProcess<?, ?> process) {
Node node = new FilterNode(process.getNodeDelegate());
objectToNodeMap.put(process, node);
return node;
}
@Override
public void stateChanged(ChangeEvent e) {
refresh(true);
}
}
}