/** * DataCleaner (community edition) * Copyright (C) 2014 Neopost - Customer Information Management * * 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<Object, JobGraphLink> 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<? extends InputColumn<?>> sourceColumns; final Collection<FilterOutcome> 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<? extends InputColumn<?>> 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<? extends InputColumn<?>> getRelevantSourceColumns( final List<? extends InputColumn<?>> sourceColumns, final ConfiguredPropertyDescriptor inputProperty) { assert inputProperty.isInputColumn(); final List<InputColumn<?>> 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<InputColumn<?>> getVisibleOutputColumns(final InputColumn<?>[] outputColumns) { final List<InputColumn<?>> 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); } }