/*OsmUi is a user interface for Osmosis Copyright (C) 2011 Verena Käfer, Peter Vollmer, Niklas Schnelle 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 3 of the License, or 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, see <http://www.gnu.org/licenses/>. */ /** * */ package de.osmui.model.pipelinemodel; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.SwingConstants; import com.mxgraph.layout.hierarchical.mxHierarchicalLayout; import com.mxgraph.model.mxCell; import com.mxgraph.view.mxGraph; import de.osmui.i18n.I18N; import de.osmui.model.exceptions.TasksNotCompatibleException; import de.osmui.model.exceptions.TasksNotInModelException; import de.osmui.util.ConfigurationManager; /** * This class implements a pipeline model using an mxGraph as a backing store * * @author Niklas Schnelle, Peter Vollmer, Verena Käfer * * @see JGPipelineModelTest * */ public class JGPipelineModel extends AbstractPipelineModel implements Serializable { private static final long serialVersionUID = -4609328085880199933L; private static final int VERTEX_WIDTH = 100; private static final int VERTEX_HEIGHT = 20; private class mxPipelineGraph extends mxGraph { // Overrides method to disallow editting @Override public boolean isCellEditable(Object cell) { return false; } @Override public boolean isCellConnectable(Object cell) { mxCell mxcell = (mxCell) cell; if (mxcell.isVertex()) { AbstractTask task = (AbstractTask) mxcell.getValue(); return task.isConnectable(); } else { return false; } } @Override public Object addCell(Object cell, Object parent, Integer index, Object source, Object target) { Object ret; ret = super.addCell(cell, parent, index, source, target); if (source != null && target != null && cell != null) { // Check the cell, which should be an edge to find out // whether the // tasks are already connected, then it has a pipe user // object mxCell mxcell = (mxCell) cell; if (mxcell.getValue() instanceof AbstractPipe) { // It's already set we are done here return ret; } mxCell mxsource = (mxCell) source; mxCell mxtarget = (mxCell) target; if (mxtarget.getValue() instanceof AbstractTask && mxsource.getValue() instanceof AbstractTask) { AbstractTask sourceTask = (AbstractTask) mxsource .getValue(); AbstractTask targetTask = (AbstractTask) mxtarget .getValue(); try { AbstractPipe pipe = rawConnectTasks(sourceTask, targetTask); mxcell.setValue(pipe); pipeMap.put(Long.valueOf(pipe.getID()), mxcell); } catch (TasksNotInModelException e) { // shouldn't happen } catch (TasksNotCompatibleException e) { // Too bad,tried connection nonsense Object[] cells = { mxcell }; removeCells(cells); return null; } } } return ret; } @Override public Object[] removeCells(Object[] cells, boolean includeEdges) { Object[] cellsRemoved = super.removeCells(cells, includeEdges); Object val; AbstractPipe pipe; for (Object cell : cellsRemoved) { val = ((mxCell) cell).getValue(); if (val instanceof AbstractPipe) { pipe = (AbstractPipe) val; pipe.disconnect(); } else if (val instanceof AbstractTask) { try { rawRemoveTask((AbstractTask) val); } catch (TasksNotInModelException e) { // Do nothing } } } return cellsRemoved; } } protected ArrayList<AbstractTask> tasks; protected Map<Long, mxCell> taskMap; protected Map<Long, mxCell> pipeMap; protected transient mxGraph graph; protected transient mxHierarchicalLayout lay; private int sourceTaskNum; /** * Constructs a new JGPipelineModel */ public JGPipelineModel() { tasks = new ArrayList<AbstractTask>(); taskMap = new HashMap<Long, mxCell>(); pipeMap = new HashMap<Long, mxCell>(); graph = new mxPipelineGraph(); sourceTaskNum = 0; lay = new mxHierarchicalLayout(graph, SwingConstants.NORTH); lay.setLayoutFromSinks(false); } /** * Restores the transient fields after deserialisation * @return */ private Object readResolve(){ graph = new mxPipelineGraph(); lay = new mxHierarchicalLayout(graph, SwingConstants.NORTH); // Add the old task cells graph.addCells(taskMap.values().toArray()); // Add the old pipe cells graph.addCells(pipeMap.values().toArray()); lay.setLayoutFromSinks(false); return this; } /** * Puts all tasks from the given model into this model * the sourceModel will get invalid * @param sourceModel */ public void setAll(JGPipelineModel sourceModel){ clean(); tasks = sourceModel.tasks; taskMap = sourceModel.taskMap; pipeMap = sourceModel.pipeMap; // We need to set this model as the associated model // for all tasks for(AbstractTask task : tasks){ task.setModel(this); } graph.addCells(taskMap.values().toArray()); graph.addCells(pipeMap.values().toArray()); setChanged(); notifyObservers(tasks.get(0)); } /** * Gets the associated graph * @return */ public mxGraph getGraph() { return this.graph; } /** * @see de.osmui.model.pipelinemodel.AbstractModel#getSourceTasks() */ @Override public List<AbstractTask> getSourceTasks() { return new ArrayList<AbstractTask>(tasks.subList(0, sourceTaskNum)); } /** * Adds the task to the model without wireing up jgraphx * @param task */ private void rawAddTask(AbstractTask task){ // Add to list of tasks, if it's a sourceTasks add to the beginning, // this speeds up getting sourceTasks if (task.getInputPorts().isEmpty()) { tasks.add(0, task); // Got a new source task sourceTaskNum++; } else { tasks.add(task); } task.setModel(this); } /** * Adds a task to jgraphx and positions it * @param task */ private void addTaskToJG(AbstractTask parentTask, AbstractTask task){ double horizontalPos = 0.0; double verticalPos = 20.0; graph.getModel().beginUpdate(); try { if(parentTask != null) { // NOTE: The child is not yet connected to the parent! mxCell parentCell = getCellForTask(parentTask); mxCell currCell = null; double currX; horizontalPos = parentCell.getGeometry().getX(); for(AbstractPipe outPipe : parentTask.getOutputPipes()){ if(outPipe.isConnected()){ currCell = (mxCell) getCellForPipe(outPipe).getTarget(); currX = currCell.getGeometry().getX(); currX -= (((double) VERTEX_WIDTH)+20.0)/2.0; currCell.getGeometry().setX(currX); horizontalPos += (((double) VERTEX_WIDTH)+20.0)/2.0; } } verticalPos += parentCell.getGeometry().getY()+VERTEX_HEIGHT+60.0; } else { // minus one to not count ourselves horizontalPos = (sourceTaskNum - 1)* (VERTEX_WIDTH+20.0) + 20.0; } // Add the task to the underling mxGraph model Object parent = graph.getDefaultParent(); // Add a mxCell to our taskMap for this task taskMap.put(Long.valueOf(task.getID()), (mxCell) graph .insertVertex(parent, null, task, horizontalPos, verticalPos, VERTEX_WIDTH, VERTEX_HEIGHT)); graph.refresh(); } finally { graph.getModel().endUpdate(); } } /** * @see de.osmui.model.pipelinemodel.AbstractModel#addTask(de.osmui.model. * pipelinemodel.AbstractTask) */ @Override public void addTask(AbstractTask task) { rawAddTask(task); addTaskToJG(null, task); setChanged(); notifyObservers(task); } /** * @see de.osmui.model.pipelinemodel.AbstractModel#addTask(de.osmui.model. * pipelinemodel.AbstractTask, de.osmui.model.pipelinemodel.AbstractTask) */ @Override public void addTask(AbstractTask parent, AbstractTask child) throws TasksNotCompatibleException, TasksNotInModelException { if (parent.getModel() != this) { throw new TasksNotInModelException(I18N.getString("JGPipelineModel.parentNotInModel")); } // First add the child and then use our internal connect method to wire // things up rawAddTask(child); addTaskToJG(parent, child); connectTasks(parent, child); if(Boolean.valueOf(ConfigurationManager.getInstance().getEntry("AutoConfCheckBox", "true"))){ layout(null); } setChanged(); notifyObservers(child); } /** * * @see * de.osmui.model.pipelinemodel.AbstractModel#removeTask(de.osmui.model. * pipelinemodel.AbstractTask) */ @Override public boolean removeTask(AbstractTask task) throws TasksNotInModelException { if (task.getModel() != this) { throw new TasksNotInModelException(I18N.getString("JGPipelineModel.taskToRmNotInModel")); } Object[] cellArray = { getCellForTask(task) }; // Our subclass of mxGraph handles disconnecting via rawRemoveTask boolean result = graph.removeCells(cellArray).length != 0; return result; } /** * @see de.osmui.model.pipelinemodel.AbstractPipelineModel#clean() */ public void clean(){ Collection<mxCell> cells = taskMap.values(); Object[] cellArray = cells.toArray(); // Our subclass of mxGraph handles disconnecting via rawRemoveTask graph.removeCells(cellArray); setChanged(); } /** * Helper method thar removes the Task from the model without affecting the * under- lying mxGraph * * @param taslk * @return * @throws TasksNotInModelException */ private boolean rawRemoveTask(AbstractTask task) throws TasksNotInModelException { if (task.getModel() != this) { throw new TasksNotInModelException(I18N.getString("JGPipelineModel.taskToRmNotInModel")); } // Disconnect all connected pipes (don't use iterator because then // we can't remove elements) int countOutputPipes = task.getOutputPipes().size(); AbstractPipe out; for(int iter=0; iter < countOutputPipes; ++iter){ out = task.getOutputPipes().get(iter); if (out.isConnected()) { out.disconnect(); } } // Disconnect all connected ports int countInputPorts = task.getInputPorts().size(); // Decrease sourceTaskNum if it was a source task if(countInputPorts == 0){ sourceTaskNum--; } AbstractPort in; for(int iter=0; iter < countInputPorts; ++iter){ in = task.getInputPorts().get(iter); if (in.isConnected()) { in.disconnect(); } } task.setModel(null); boolean result = tasks.remove(task); setChanged(); notifyObservers(task); return result; } /* * (non-Javadoc) * * @see * de.osmui.model.pipelinemodel.AbstractModel#connectTasks(de.osmui.model * .pipelinemodel.AbstractTask, de.osmui.model.pipelinemodel.AbstractTask) */ @Override public AbstractPipe connectTasks(AbstractTask parent, AbstractTask child) throws TasksNotCompatibleException, TasksNotInModelException { Object graphparent = graph.getDefaultParent(); // Thanks to our subclass of mxGraph this will make the model // connection. // see overwritten addCell mxCell edge = (mxCell) graph.insertEdge(graphparent, null, null, getCellForTask(parent), getCellForTask(child)); if(edge == null){ throw new TasksNotCompatibleException(I18N.getString("JGPipelineModel.TasksNotComp")); } setChanged(); notifyObservers(edge.getValue()); return (AbstractPipe) edge.getValue(); } /** * This helper method connects tasks without adding their connection to the * mxGraph * * @param parent * @param child * @return * @throws TasksNotCompatibleException * @throws TasksNotInModelException */ private AbstractPipe rawConnectTasks(AbstractTask parent, AbstractTask child) throws TasksNotCompatibleException, TasksNotInModelException { return super.connectTasks(parent, child); } /* * (non-Javadoc) * * @see * de.osmui.model.pipelinemodel.AbstractPipelineModel#connectTasks(de.osmui * .model.pipelinemodel.AbstractPipe, * de.osmui.model.pipelinemodel.AbstractPort) */ public AbstractPipe connectTasks(AbstractPipe output, AbstractPort input) throws TasksNotCompatibleException, TasksNotInModelException { // Make the normal connection AbstractPipe pipe = super.connectTasks(output, input); // We need to get the corresponding tasks from our task list to make // sure we get the decorated version AbstractTask parent = output.getSource(); AbstractTask child = input.getParent(); // Setup the jgraphx madness Object graphparent = graph.getDefaultParent(); graph.getModel().beginUpdate(); try { pipeMap.put(Long.valueOf(pipe.getID()), (mxCell) graph.insertEdge(graphparent, null, pipe, getCellForTask(parent), getCellForTask(child))); } finally { graph.getModel().endUpdate(); } setChanged(); notifyObservers(pipe); return pipe; } /* * (non-Javadoc) * * @see * de.osmui.model.pipelinemodel.AbstractModel#disconnectTasks(de.osmui.model * .pipelinemodel.AbstractTask, de.osmui.model.pipelinemodel.AbstractTask) */ @Override public AbstractPipe disconnectTasks(AbstractTask parent, AbstractTask child) throws TasksNotInModelException { AbstractPipe removedPipe = super.disconnectTasks(parent, child); Object[] cellArray = { getCellForPipe(removedPipe) }; graph.removeCells(cellArray); return removedPipe; } /* * (non-Javadoc) * * @see de.osmui.model.pipelinemodel.AbstractModel#isExecutable() */ @Override public boolean isExecutable() { // Check whether all pipes are connected for(AbstractTask task : tasks){ // Check inputs for(AbstractPort port : task.getInputPorts()){ if(!port.isConnected()){ return false; } } // Check pipes for(AbstractPipe pipe : task.getOutputPipes()){ if(!pipe.isConnected()){ return false; } } } return true; } /* * (non-Javadoc) * * @see de.osmui.model.pipelinemodel.AbstractModel#isEmpty() */ @Override public boolean isEmpty() { return tasks.isEmpty(); } public void layout(AbstractTask parent) { Object graphparent = (parent != null) ? pipeMap.get(Long.valueOf(parent .getID())) : graph.getDefaultParent(); graph.getModel().beginUpdate(); try { lay.execute(graphparent); } finally { graph.getModel().endUpdate(); } } public mxCell getCellForTask(AbstractTask task){ return taskMap.get(Long.valueOf(task.getID())); } public mxCell getCellForPipe(AbstractPipe pipe){ return pipeMap.get(Long.valueOf(pipe.getID())); } }