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

org.datacleaner.widgets.visualization.JobGraphLinkPainter Maven / Gradle / Ivy

/**
 * DataCleaner (community edition)
 * Copyright (C) 2014 Free Software Foundation, Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.datacleaner.widgets.visualization;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;

import org.apache.metamodel.schema.Table;
import org.datacleaner.api.ColumnProperty;
import org.datacleaner.api.InputColumn;
import org.datacleaner.api.OutputDataStream;
import org.datacleaner.data.MutableInputColumn;
import org.datacleaner.descriptors.ConfiguredPropertyDescriptor;
import org.datacleaner.job.ComponentRequirement;
import org.datacleaner.job.CompoundComponentRequirement;
import org.datacleaner.job.FilterOutcome;
import org.datacleaner.job.HasFilterOutcomes;
import org.datacleaner.job.InputColumnSourceJob;
import org.datacleaner.job.SimpleComponentRequirement;
import org.datacleaner.job.builder.AnalysisJobBuilder;
import org.datacleaner.job.builder.ComponentBuilder;
import org.datacleaner.util.GraphUtils;
import org.datacleaner.util.IconUtils;
import org.datacleaner.util.LabelUtils;
import org.datacleaner.util.ReflectionUtils;
import org.datacleaner.util.WidgetFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import edu.uci.ics.jung.algorithms.layout.AbstractLayout;
import edu.uci.ics.jung.visualization.VisualizationServer;

/**
 * Supporting class containing the state surrounding the drawing of new
 * {@link JobGraphLink}s.
 */
public class JobGraphLinkPainter {
    public static class VertexContext {
        private final Object _vertex;
        private final OutputDataStream _outputDataStream;
        private final AnalysisJobBuilder _analysisJobBuilder;

        public VertexContext(final Object vertex, final AnalysisJobBuilder analysisJobBuilder,
                final OutputDataStream outputDataStream) {
            _vertex = vertex;
            _outputDataStream = outputDataStream;
            _analysisJobBuilder = analysisJobBuilder;
        }

        public Object getVertex() {
            return _vertex;
        }

        public OutputDataStream getOutputDataStream() {
            return _outputDataStream;
        }

        public AnalysisJobBuilder getAnalysisJobBuilder() {
            return _analysisJobBuilder;
        }
    }

    /**
     * Used for the edge creation visual effect during mouse drag
     */
    class EdgePaintable implements VisualizationServer.Paintable {

        public void paint(final Graphics graphics) {
            if (_edgeShape != null) {
                final Color oldColor = graphics.getColor();
                graphics.setColor(Color.black);
                ((Graphics2D) graphics).draw(_edgeShape);
                graphics.setColor(oldColor);
            }
        }

        public boolean useTransform() {
            return false;
        }
    }

    /**
     * Used for the directed edge creation visual effect during mouse drag
     */
    class ArrowPaintable implements VisualizationServer.Paintable {

        public void paint(final Graphics graphics) {
            if (_arrowShape != null) {
                final Color oldColor = graphics.getColor();
                graphics.setColor(Color.black);
                ((Graphics2D) graphics).fill(_arrowShape);
                graphics.setColor(oldColor);
            }
        }

        public boolean useTransform() {
            return false;
        }
    }

    private static final Logger logger = LoggerFactory.getLogger(JobGraphLinkPainter.class);
    private final JobGraphContext _graphContext;
    private final JobGraphActions _actions;
    private final VisualizationServer.Paintable _edgePaintable;
    private final VisualizationServer.Paintable _arrowPaintable;
    private Shape _edgeShape;
    private Shape _arrowShape;
    private VertexContext _startVertex;
    private Point2D _startPoint;

    public JobGraphLinkPainter(final JobGraphContext graphContext, final JobGraphActions actions) {
        _graphContext = graphContext;
        _actions = actions;
        _edgePaintable = new EdgePaintable();
        _arrowPaintable = new ArrowPaintable();
    }

    /**
     * Called when the drawing of a new link/edge is started
     *
     * @param startVertex
     */
    public void startLink(final VertexContext startVertex) {
        if (startVertex == null) {
            return;
        }

        final AbstractLayout graphLayout = _graphContext.getGraphLayout();
        final int x = (int) graphLayout.getX(startVertex.getVertex());
        final int y = (int) graphLayout.getY(startVertex.getVertex());

        logger.debug("startLink({})", startVertex);

        _startVertex = startVertex;
        _startPoint = new Point(x, y);

        transformEdgeShape(_startPoint, _startPoint);
        _graphContext.getVisualizationViewer().addPostRenderPaintable(_edgePaintable);
        transformArrowShape(_startPoint, _startPoint);
        _graphContext.getVisualizationViewer().addPostRenderPaintable(_arrowPaintable);
    }

    public boolean endLink(final MouseEvent me) {
        if (_startVertex != null) {
            final Object vertex = _graphContext.getVertex(me);
            return endLink(vertex, me);
        }
        return false;
    }

    /**
     * If startVertex is non-null this method will attempt to end the
     * link-painting at the given endVertex
     *
     * @return true if a link drawing was ended or false if it wasn't started
     */
    public boolean endLink(final Object endVertex, final MouseEvent mouseEvent) {
        logger.debug("endLink({})", endVertex);
        boolean result = false;
        if (_startVertex != null && endVertex != null) {
            if (mouseEvent.getButton() == MouseEvent.BUTTON1) {
                final boolean created = createLink(_startVertex, endVertex, mouseEvent);
                if (created && _graphContext.getVisualizationViewer().isVisible()) {
                    _graphContext.getJobGraph().refresh();
                }
                result = true;
            }
        }
        stopDrawing();
        return result;
    }

    private void stopDrawing() {
        _startVertex = null;
        _startPoint = null;
        _graphContext.getVisualizationViewer().removePostRenderPaintable(_edgePaintable);
        _graphContext.getVisualizationViewer().removePostRenderPaintable(_arrowPaintable);
    }

    /**
     * Cancels the drawing of the link
     */
    public void cancelLink() {
        logger.debug("cancelLink()");
        stopDrawing();
    }

    public void moveCursor(final MouseEvent me) {
        if (_startVertex != null) {
            moveCursor(me.getPoint());
        }
    }

    public void moveCursor(final Point2D currentPoint) {
        if (_startVertex != null) {
            logger.debug("moveCursor({})", currentPoint);
            transformEdgeShape(_startPoint, currentPoint);
            transformArrowShape(_startPoint, currentPoint);
            _graphContext.getVisualizationViewer().repaint();
        }
    }

    private boolean createLink(final VertexContext fromVertex, final Object toVertex, final MouseEvent mouseEvent) {
        logger.debug("createLink({}, {}, {})", fromVertex, toVertex, mouseEvent);

        final List> sourceColumns;
        final Collection filterOutcomes;

        final AnalysisJobBuilder sourceAnalysisJobBuilder = fromVertex.getAnalysisJobBuilder();

        if (fromVertex.getOutputDataStream() != null) {
            sourceColumns = sourceAnalysisJobBuilder.getSourceColumns();
            filterOutcomes = null;
        } else if (fromVertex.getVertex() instanceof Table) {
            final Table table = (Table) fromVertex.getVertex();
            sourceColumns = sourceAnalysisJobBuilder.getSourceColumnsOfTable(table);
            filterOutcomes = null;
        } else if (fromVertex.getVertex() instanceof InputColumnSourceJob) {
            InputColumn[] outputColumns;
            try {
                outputColumns = ((InputColumnSourceJob) fromVertex.getVertex()).getOutput();
            } catch (final Exception e) {
                outputColumns = new InputColumn[0];
            }
            sourceColumns = getVisibleOutputColumns(outputColumns);
            filterOutcomes = null;
        } else if (fromVertex.getVertex() instanceof HasFilterOutcomes) {
            final HasFilterOutcomes hasFilterOutcomes = (HasFilterOutcomes) fromVertex.getVertex();
            filterOutcomes = hasFilterOutcomes.getFilterOutcomes();
            sourceColumns = null;
        } else {
            sourceColumns = null;
            filterOutcomes = null;
        }
        if (toVertex instanceof ComponentBuilder) {
            final ComponentBuilder componentBuilder = (ComponentBuilder) toVertex;

            if (sourceColumns != null && !sourceColumns.isEmpty()) {
                if (componentBuilder.getDescriptor().isMultiStreamComponent()) {
                    if (!fromVertex.getAnalysisJobBuilder().isRootJobBuilder()) {
                        // we don't yet support MultiStreamComponents on output
                        // data streams. See issue #620
                        return false;
                    }
                }

                if (!scopeUpdatePermitted(sourceAnalysisJobBuilder, componentBuilder)) {
                    return false;
                }

                sourceAnalysisJobBuilder.moveComponent(componentBuilder);

                try {

                    final ConfiguredPropertyDescriptor inputProperty =
                            componentBuilder.getDefaultConfiguredPropertyForInput();

                    final ColumnProperty columnProperty = inputProperty.getAnnotation(ColumnProperty.class);
                    if (inputProperty.isArray() || (columnProperty != null && columnProperty
                            .escalateToMultipleJobs())) {
                        componentBuilder
                                .addInputColumns(getRelevantSourceColumns(sourceColumns, inputProperty), inputProperty);
                    } else {
                        final InputColumn firstRelevantSourceColumn =
                                getFirstRelevantSourceColumn(sourceColumns, inputProperty);
                        if (firstRelevantSourceColumn != null) {
                            componentBuilder.setConfiguredProperty(inputProperty, firstRelevantSourceColumn);
                        }
                    }
                    _actions.showConfigurationDialog(componentBuilder);

                    // returning true to indicate a change
                    logger.debug("createLink(...) returning true - input column(s) added");
                    return true;
                } catch (final Exception e) {
                    // nothing to do
                    logger.info("Failed to add input columns ({}) to {}", sourceColumns.size(), componentBuilder, e);
                }
            } else if (filterOutcomes != null && !filterOutcomes.isEmpty()) {
                final JPopupMenu popup = new JPopupMenu();
                for (final FilterOutcome filterOutcome : filterOutcomes) {
                    final JMenuItem menuItem =
                            WidgetFactory.createMenuItem(filterOutcome.getSimpleName(), IconUtils.FILTER_OUTCOME_PATH);
                    menuItem.addActionListener(e -> {
                        if (scopeUpdatePermitted(sourceAnalysisJobBuilder, componentBuilder)) {
                            sourceAnalysisJobBuilder.moveComponent(componentBuilder);
                            addOrSetFilterOutcomeAsRequirement(componentBuilder, filterOutcome);
                        }
                    });
                    popup.add(menuItem);
                }
                popup.show(_graphContext.getVisualizationViewer(), mouseEvent.getX(), mouseEvent.getY());

                // we return false because no change was applied (yet)
                logger.debug("createLink(...) returning false - popup with choices presented to user");
                return false;
            }

            // When we can't do anything, at least show the dialog.
            _actions.showConfigurationDialog(componentBuilder);
        }
        logger.debug("createLink(...) returning false - no applicable action");
        return false;
    }

    /**
     * This will check if components are in a different scope, and ask the user
     * for permission to change the scope of the target component
     *
     * @return true if permitted or irrelevant, false if user refused a
     *         necessary scope change.
     */
    private boolean scopeUpdatePermitted(final AnalysisJobBuilder sourceAnalysisJobBuilder,
            final ComponentBuilder componentBuilder) {
        if (sourceAnalysisJobBuilder != componentBuilder.getAnalysisJobBuilder()) {
            if (componentBuilder.getInput().length > 0 || componentBuilder.getComponentRequirement() != null) {
                final String scopeText;
                scopeText = LabelUtils.getScopeLabel(sourceAnalysisJobBuilder);
                final int response = JOptionPane.showConfirmDialog(_graphContext.getVisualizationViewer(),
                        "This will move " + LabelUtils.getLabel(componentBuilder) + " into the " + scopeText
                                + ", thereby losing its configured columns and/or requirements", "Change scope?",
                        JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);

                if (response == JOptionPane.CANCEL_OPTION) {
                    _graphContext.getJobGraph().refresh();
                    return false;
                }
            }
        }
        return true;
    }

    protected void addOrSetFilterOutcomeAsRequirement(final ComponentBuilder componentBuilder,
            final FilterOutcome filterOutcome) {
        final ComponentRequirement existingRequirement = componentBuilder.getComponentRequirement();
        if (existingRequirement == null) {
            // set a new requirement
            final ComponentRequirement requirement = new SimpleComponentRequirement(filterOutcome);
            componentBuilder.setComponentRequirement(requirement);
            return;
        }

        final ComponentRequirement defaultRequirement =
                componentBuilder.getAnalysisJobBuilder().getDefaultRequirement();
        if (existingRequirement.equals(defaultRequirement)) {
            // override the default requirement
            final ComponentRequirement requirement = new SimpleComponentRequirement(filterOutcome);
            componentBuilder.setComponentRequirement(requirement);
            return;
        }

        // add outcome to a compound requirement
        final CompoundComponentRequirement requirement =
                new CompoundComponentRequirement(existingRequirement, filterOutcome);
        componentBuilder.setComponentRequirement(requirement);
    }

    private InputColumn getFirstRelevantSourceColumn(final List> sourceColumns,
            final ConfiguredPropertyDescriptor inputProperty) {
        assert inputProperty.isInputColumn();

        final Class expectedDataType = inputProperty.getTypeArgument(0);
        for (final InputColumn inputColumn : sourceColumns) {
            final Class actualDataType = inputColumn.getDataType();
            if (ReflectionUtils.is(actualDataType, expectedDataType, false)) {
                return inputColumn;
            }
        }

        return null;
    }

    private Collection> getRelevantSourceColumns(
            final List> sourceColumns, final ConfiguredPropertyDescriptor inputProperty) {
        assert inputProperty.isInputColumn();

        final List> result = new ArrayList<>();
        final Class expectedDataType = inputProperty.getTypeArgument(0);
        for (final InputColumn inputColumn : sourceColumns) {
            final Class actualDataType = inputColumn.getDataType();
            if (ReflectionUtils.is(actualDataType, expectedDataType, false)) {
                result.add(inputColumn);
            }
        }

        return result;
    }

    private List> getVisibleOutputColumns(final InputColumn[] outputColumns) {
        final List> visibleColumns = new ArrayList<>();
        for (int i = 0; i < outputColumns.length; i++) {
            if (outputColumns[i] instanceof MutableInputColumn) {
                final MutableInputColumn mutableOutputColum = (MutableInputColumn) outputColumns[i];
                if (!mutableOutputColum.isHidden()) {
                    visibleColumns.add(mutableOutputColum);
                }
            } else {
                visibleColumns.add(outputColumns[i]);
            }
        }
        return visibleColumns;
    }

    private void transformEdgeShape(final Point2D down, final Point2D out) {
        _edgeShape = new Line2D.Float(down, out);
    }

    private void transformArrowShape(final Point2D down, final Point2D out) {
        final float x1 = (float) down.getX();
        final float y1 = (float) down.getY();
        final float x2 = (float) out.getX();
        final float y2 = (float) out.getY();

        final AffineTransform xform = AffineTransform.getTranslateInstance(x2, y2);

        final float dx = x2 - x1;
        final float dy = y2 - y1;
        final float thetaRadians = (float) Math.atan2(dy, dx);
        xform.rotate(thetaRadians);
        _arrowShape = xform.createTransformedShape(GraphUtils.ARROW_SHAPE);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy