All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cayenne.modeler.graph.BaseGraphBuilder Maven / Gradle / Ivy

There is a newer version: 5.0-M1
Show newest version
/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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
 *
 *    https://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.apache.cayenne.modeler.graph;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.swing.JPopupMenu;
import javax.swing.border.LineBorder;
import javax.swing.event.UndoableEditEvent;

import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
import org.apache.cayenne.configuration.DataChannelDescriptor;
import org.apache.cayenne.configuration.event.DataMapEvent;
import org.apache.cayenne.configuration.event.DataMapListener;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.Entity;
import org.apache.cayenne.map.Relationship;
import org.apache.cayenne.map.event.EntityEvent;
import org.apache.cayenne.map.event.RelationshipEvent;
import org.apache.cayenne.modeler.Application;
import org.apache.cayenne.modeler.ProjectController;
import org.apache.cayenne.modeler.action.ActionManager;
import org.apache.cayenne.modeler.action.CreateAttributeAction;
import org.apache.cayenne.modeler.action.CreateRelationshipAction;
import org.apache.cayenne.modeler.graph.action.EntityDisplayAction;
import org.apache.cayenne.modeler.graph.action.RemoveEntityAction;
import org.apache.cayenne.util.XMLEncoder;
import org.jgraph.JGraph;
import org.jgraph.graph.AttributeMap;
import org.jgraph.graph.DefaultCellViewFactory;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;
import org.jgraph.graph.GraphModel;

import com.jgraph.layout.JGraphFacade;
import com.jgraph.layout.organic.JGraphOrganicLayout;

/**
 * Base class for building graphs of entities
 */
abstract class BaseGraphBuilder implements GraphBuilder, DataMapListener {

    static final Font EDGE_FONT = new Font("Verdana", Font.PLAIN, 10);

    /**
     * Graph
     */
    protected JGraph graph;

    /**
     * Domain
     */
    protected transient DataChannelDescriptor domain;

    /**
     * Created entity cells. Maps to entity name, since GraphBuilder can be
     * serialized
     */
    protected Map entityCells;

    /**
     * Created relationship cells Maps to relationship qualified name, since
     * GraphBuilder can be serialized
     */
    protected Map relCells;

    /**
     * Created non-isolated objects
     */
    protected List createdObjects;

    /**
     * Current project controller
     */
    protected transient ProjectController mediator;

    protected transient Entity selectedEntity;

    transient JPopupMenu popup;

    boolean undoEventsDisabled;

    @Override
    public void buildGraph(ProjectController mediator, DataChannelDescriptor domain, boolean doLayout) {
        if (graph != null) {
            // graph already built, exiting silently
            return;
        }

        graph = new JGraph();
        GraphModel model = new DefaultGraphModel();
        graph.setModel(model);

        setProjectController(mediator);
        setDataDomain(domain);

        GraphLayoutCache view = new GraphLayoutCache(model, new DefaultCellViewFactory());
        graph.setGraphLayoutCache(view);

        addMouseListeners();

        entityCells = new HashMap<>();
        createdObjects = new ArrayList<>();
        relCells = new HashMap<>();

        /*
         * an array for entities that are not connected to anyone. We add them
         * separately so that layout doesn't touch them
         */
        List isolatedObjects = new ArrayList<>();

        /*
         * 1. Add all entities
         */
        for (DataMap map : domain.getDataMaps()) {
            DefaultGraphCell mapCell = new DefaultGraphCell();
            createdObjects.add(mapCell);

            for (Entity entity : getEntities(map)) {
                DefaultGraphCell cell = createEntityCell(entity);

                // mapCell.add(cell);
                // cell.setParent(mapCell);

                List array = !isIsolated(domain, entity) ? createdObjects : isolatedObjects;
                array.add(cell);
                array.add((DefaultGraphCell) cell.getChildAt(0)); // port
            }
        }

        /*
         * 2. Add all relationships
         */
        for (DataMap map : domain.getDataMaps()) {
            for (Entity entity : getEntities(map)) {
                DefaultGraphCell sourceCell = entityCells.get(entity.getName());

                postProcessEntity(entity, sourceCell);
            }
        }
        view.insert(createdObjects.toArray());

        setLayout(doLayout);

        /*
         * Adding isolated objects
         * 
         * We're placing them so that they will take maximum space in left top
         * corner. The sample order is below:
         * 
         * 1 2 6 7... 3 5 8 ... 4 9... 10 ...
         */
        addIsolatedObjects(isolatedObjects);

        view.insert(isolatedObjects.toArray());
        graph.getModel().addUndoableEditListener(this);
    }

	private void setLayout(boolean doLayout) {
		if (doLayout) {
            JGraphFacade facade = new JGraphFacade(graph);

            JGraphOrganicLayout layout = new JGraphOrganicLayout();
            layout.setNodeDistributionCostFactor(5000000000000.0);
            layout.setEdgeLengthCostFactor(1000);
            layout.setEdgeCrossingCostFactor(1000000);
            layout.setOptimizeBorderLine(false);
            layout.setOptimizeEdgeDistance(false);

            // JGraphHierarchicalLayout layout = new JGraphHierarchicalLayout();
            // layout.setInterHierarchySpacing(150.0);
            // layout.setIntraCellSpacing(150.0);
            // layout.setInterRankCellSpacing(150.0);

            // JGraphSimpleLayout layout = new JGraphSimpleLayout(JGraphSimpleLayout.TYPE_TILT, 4000, 2000);
            layout.run(facade);
            // Obtain a map of the resulting attribute changes from the facade
            Map nested = facade.createNestedMap(true, true);

            // Apply the results to the actual graph
            edit(nested);
        }
	}

	private void addMouseListeners() {
		graph.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    Object selected = graph.getSelectionCell();
                    if (selected != null && selected instanceof DefaultGraphCell) {
                        Object userObject = ((DefaultGraphCell) selected).getUserObject();
                        if (userObject instanceof EntityCellMetadata) {
                            showPopup(e.getPoint(), ((EntityCellMetadata) userObject).fetchEntity());
                        }
                    }
                }
            }
        });
		
		graph.addMouseWheelListener(e -> {
            // limit scale
            double scale = graph.getScale() / Math.pow(ZOOM_FACTOR, e.getWheelRotation());
            scale = Math.max(scale, 0.1);
            scale = Math.min(scale, 3);
            graph.setScale(scale);
        });
	}

	private void addIsolatedObjects(List isolatedObjects) {
		if (isolatedObjects.size() > 0) {
            int n = isolatedObjects.size() / 2; // number of isolated entities
            int x = (int) Math.ceil((Math.sqrt(1 + 8 * n) - 1) / 2); // side of
                                                                     // triangle

            Dimension pref = graph.getPreferredSize();
            int dx = pref.width / 2 / x; // x-distance between entities
            int dy = pref.height / 2 / x; // y-distance between entities

            int posX = dx / 2;
            int posY = dy / 2;

            int row = 0;

            for (int isolatedIndex = 0; isolatedIndex < isolatedObjects.size();) {
                for (int i = 0; isolatedIndex < isolatedObjects.size() && i < x - row; i++) {
                    GraphConstants.setBounds(isolatedObjects.get(isolatedIndex).getAttributes(),
                            new Rectangle2D.Double(pref.width - posX, pref.height - 3 * posY / 2, 10, 10));
                    isolatedIndex += 2; // because every 2nd object is port
                    posX += dx;
                }
                posX = dx / 2;
                posY += dy / 2;
                row++;
            }
        }
	}

    protected DefaultGraphCell createEntityCell(Entity entity) {
        DefaultGraphCell cell = new DefaultGraphCell(getCellMetadata(entity));

        GraphConstants.setResize(cell.getAttributes(), true);
        GraphConstants.setBorder(cell.getAttributes(), new LineBorder(Color.BLACK));

        GraphConstants.setEditable(cell.getAttributes(), false);
        entityCells.put(entity.getName(), cell);

        cell.addPort();
        return cell;
    }

    @Override
    public DefaultGraphCell getEntityCell(String entityName) {
        return entityCells.get(entityName);
    }

    /**
     * Post (i.e. after creation on entity cell) process of the entity
     */
    protected void postProcessEntity(Entity entity, DefaultGraphCell cell) {
        for (Relationship rel : entity.getRelationships()) {
            if (rel.getSourceEntity() != null && rel.getTargetEntity() != null) {
                DefaultEdge edge = createRelationshipCell(rel);
                if (edge != null) {
                    createdObjects.add(edge);
                }
            }
        }
    }

    /**
     * Returns whether an entity is not connected to any other TODO: not fine
     * algorithm, it iterates through all entities and all rels
     */
    protected boolean isIsolated(DataChannelDescriptor domain, Entity entity) {
        if (entity.getRelationships().size() == 0) {
            // searching for rels that have a target="entity"

            for (DataMap map : domain.getDataMaps()) {
                for (Entity source : getEntities(map)) {
                    if (source.getAnyRelationship(entity) != null) {
                        return false;
                    }
                }
            }
            return true;
        }
        return false;
    }

    protected abstract Collection getEntities(DataMap map);

    /**
     * Returns label for relationship on the graph, considering its "mandatory"
     * and "to-many" properties
     */
    private static String getRelationshipLabel(Relationship rel) {
        if (rel == null) {
            return null;
        }

        if (rel.isToMany()) {
            return "0..*";
        }
        return rel.isMandatory() ? "1" : "0..1";
    }

    /**
     * Returns metadata (user object) for this cell
     */
    protected abstract EntityCellMetadata getCellMetadata(Entity e);

    protected void showPopup(Point p, Entity entity) {
        selectedEntity = entity;
        if (popup == null) {
            popup = createPopupMenu();
        }
        popup.show(graph, p.x, p.y);
    }

    @Override
    public Entity getSelectedEntity() {
        return selectedEntity;
    }

    /**
     * Creates popup menu
     */
    protected JPopupMenu createPopupMenu() {
        ActionManager actionManager = Application.getInstance().getActionManager();

        JPopupMenu menu = new JPopupMenu();
        menu.add(new EntityDisplayAction(this).buildMenu());
        menu.addSeparator();
        menu.add(new EntityDisplayAction(this, actionManager.getAction(CreateAttributeAction.class)).buildMenu());
        menu.add(new EntityDisplayAction(this, actionManager.getAction(CreateRelationshipAction.class)).buildMenu());
        menu.addSeparator();
        menu.add(new RemoveEntityAction(this));

        return menu;
    }

    /**
     * Updates specified entity on the graph
     */
    protected void updateEntityCell(Entity e) {
        DefaultGraphCell cell = entityCells.get(e.getName());
        if (cell != null) {
            GraphConstants.setValue(cell.getAttributes(), getCellMetadata(e));
            GraphConstants.setResize(cell.getAttributes(), true);

            Map nested = new HashMap<>();
            nested.put(cell, cell.getAttributes());

            edit(nested);
        }
    }

    protected void updateRelationshipCell(Relationship rel) {
        if (rel.getSourceEntity() != null && rel.getTargetEntity() != null) {
            DefaultEdge edge = relCells.get(getQualifiedName(rel));
            if (edge != null) {
                updateRelationshipLabels(edge, rel, rel.getReverseRelationship());

                Map nested = new HashMap<>();
                nested.put(edge, edge.getAttributes());
                edit(nested);
            } else {
                insertRelationshipCell(rel);
            }
        }
    }

    protected void removeEntityCell(Entity e) {
        final DefaultGraphCell cell = entityCells.get(e.getName());
        if (cell != null) {
            runWithUndoDisabled(() -> graph.getGraphLayoutCache().remove(new Object[] { cell }, true, true));
            entityCells.remove(e.getName());
        }
    }

    protected void removeRelationshipCell(Relationship rel) {
        final DefaultEdge edge = relCells.get(getQualifiedName(rel));
        if (edge != null) {
            runWithUndoDisabled(() -> graph.getGraphLayoutCache().remove(new Object[] { edge }));
            relCells.remove(getQualifiedName(rel));
        }
    }

    protected DefaultEdge createRelationshipCell(Relationship rel) {
        if (!relCells.containsKey(getQualifiedName(rel))) {
            Relationship reverse = rel.getReverseRelationship();

            DefaultEdge edge = new DefaultEdge();

            // GraphConstants.setLineStyle(edge.getAttributes(),
            // GraphConstants.STYLE_ORTHOGONAL);
            // GraphConstants.setRouting(edge.getAttributes(),
            // GraphConstants.ROUTING_SIMPLE);

            GraphConstants.setEditable(edge.getAttributes(), false);
            GraphConstants.setLabelAlongEdge(edge.getAttributes(), true);
            GraphConstants.setSelectable(edge.getAttributes(), false);
            GraphConstants.setFont(edge.getAttributes(), EDGE_FONT);

            updateRelationshipLabels(edge, rel, reverse);

            relCells.put(getQualifiedName(rel), edge);

            if (reverse != null) {
                relCells.put(getQualifiedName(reverse), edge);
            }

            return edge;
        }
        return null;
    }

    protected void insertRelationshipCell(Relationship rel) {
        DefaultEdge edge = createRelationshipCell(rel);
        insert(edge);
    }

    protected void insertEntityCell(Entity entity) {
        DefaultGraphCell cell = createEntityCell(entity);

        // putting cell to a random posistion..
        GraphConstants.setBounds(cell.getAttributes(),
                new Rectangle2D.Double(Math.random() * graph.getWidth(), Math.random() * graph.getHeight(), 10, 10));

        // setting graph type-specific attrs
        postProcessEntity(entity, cell);

        insert(cell);
    }

    /**
     * Updates relationship labels for specified relationship edge.
     */
    protected void updateRelationshipLabels(DefaultEdge edge, Relationship rel, Relationship reverse) {
        DefaultGraphCell sourceCell = entityCells.get(rel.getSourceEntity().getName());
        DefaultGraphCell targetCell = entityCells.get(rel.getTargetEntity().getName());

        edge.setSource(sourceCell != null ? sourceCell.getChildAt(0) : null);
        edge.setTarget(targetCell != null ? targetCell.getChildAt(0) : null);

        Object[] labels = { rel.getName() + " " + getRelationshipLabel(rel),
                reverse == null ? "" : reverse.getName() + " " + getRelationshipLabel(reverse) };
        GraphConstants.setExtraLabels(edge.getAttributes(), labels);

        Point2D[] labelPositions = {
                new Point2D.Double(GraphConstants.PERMILLE * (0.1 + 0.2 * Math.random()), 10),
                new Point2D.Double(GraphConstants.PERMILLE * (0.9 - 0.2 * Math.random()), -10)
        };
        GraphConstants.setExtraLabelPositions(edge.getAttributes(), labelPositions);
    }

    @Override
    public JGraph getGraph() {
        return graph;
    }

    @Override
    public void dataMapAdded(DataMapEvent e) {
    }

    @Override
    public void dataMapChanged(DataMapEvent e) {
    }

    @Override
    public void dataMapRemoved(DataMapEvent e) {
        for (Entity entity : getEntities(e.getDataMap())) {
            removeEntityCell(entity);
        }
    }

    public void setProjectController(ProjectController mediator) {
        this.mediator = mediator;

        mediator.addDataMapListener(this);
    }

    public void setDataDomain(DataChannelDescriptor domain) {
        this.domain = domain;
    }

    @Override
    public DataChannelDescriptor getDataDomain() {
        return domain;
    }

    @Override
    public void destroy() {
        mediator.removeDataMapListener(this);
    }

    /**
     * Checks if entity name has changed, then changes map key
     */
    protected void remapEntity(EntityEvent e) {
        if (e.isNameChange()) {
            entityCells.put(e.getNewName(), entityCells.remove(e.getOldName()));
        }
    }

    /**
     * Checks if entity name has changed, then changes map key
     */
    protected void remapRelationship(RelationshipEvent e) {
        if (e.isNameChange()) {
            relCells.put(getQualifiedName(e.getRelationship()),
                    relCells.remove(e.getEntity().getName() + "." + e.getOldName()));
        }
    }

    /**
     * Returns qualified name (entity name + relationship name) for a
     * relationship
     */
    static String getQualifiedName(Relationship rel) {
        return rel.getSourceEntity().getName() + "." + rel.getName();
    }

    @Override
    public void encodeAsXML(XMLEncoder encoder, ConfigurationNodeVisitor delegate) {
        encoder.start("graph")
                .attribute("type", getType().toString())
                .attribute("scale", String.valueOf(graph.getScale()));

        for (Entry entry : entityCells.entrySet()) {
            Rectangle2D rect = graph.getCellBounds(entry.getValue());

            encoder.start("entity")
                    .attribute("name", entry.getKey())
                    .attribute("x", String.valueOf(Math.round(100 * rect.getX()) / 100.0))
                    .attribute("y", String.valueOf(Math.round(100 * rect.getY()) / 100.0))
                    .attribute("width", String.valueOf(rect.getWidth()))
                    .attribute("height", String.valueOf(rect.getHeight()))
                    .end();
        }

        encoder.end();
    }

    private void edit(final Map map) {
        runWithUndoDisabled(() -> graph.getGraphLayoutCache().edit(map));
    }

    private void insert(final Object cell) {
        runWithUndoDisabled(() -> graph.getGraphLayoutCache().insert(cell));
    }

    private void runWithUndoDisabled(Runnable r) {
        undoEventsDisabled = true;
        try {
            r.run();
        } finally {
            undoEventsDisabled = false;
        }
    }

    @Override
    public void undoableEditHappened(UndoableEditEvent e) {
        if (!undoEventsDisabled) {
            // graph has been modified
            mediator.setDirty(true);

            Application.getInstance().getUndoManager().undoableEditHappened(e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy