/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2017 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * 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 org.pentaho.di.trans; import java.util.List; import java.util.Map; import org.pentaho.di.core.Const; import org.pentaho.di.core.util.Utils; import org.pentaho.di.core.NotePadMeta; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.extension.ExtensionPointHandler; import org.pentaho.di.core.extension.KettleExtensionPoint; import org.pentaho.di.core.gui.AreaOwner; import org.pentaho.di.core.gui.AreaOwner.AreaType; import org.pentaho.di.core.gui.BasePainter; import org.pentaho.di.core.gui.GCInterface; import org.pentaho.di.core.gui.Point; import org.pentaho.di.core.gui.PrimitiveGCInterface.EColor; import org.pentaho.di.core.gui.PrimitiveGCInterface.EFont; import org.pentaho.di.core.gui.PrimitiveGCInterface.EImage; import org.pentaho.di.core.gui.PrimitiveGCInterface.ELineStyle; import org.pentaho.di.core.gui.Rectangle; import org.pentaho.di.core.gui.ScrollBarInterface; import org.pentaho.di.core.injection.bean.BeanInjectionInfo; import org.pentaho.di.core.logging.LogChannel; import org.pentaho.di.i18n.BaseMessages; import org.pentaho.di.partition.PartitionSchema; import org.pentaho.di.trans.step.BaseStepData.StepExecutionStatus; import org.pentaho.di.trans.step.StepIOMetaInterface; import org.pentaho.di.trans.step.StepInterface; import org.pentaho.di.trans.step.StepMeta; import org.pentaho.di.trans.step.StepMetaInterface; import org.pentaho.di.trans.step.StepPartitioningMeta; import org.pentaho.di.trans.step.StepStatus; import org.pentaho.di.trans.step.errorhandling.StreamInterface; import org.pentaho.di.trans.step.errorhandling.StreamInterface.StreamType; public class TransPainter extends BasePainter<TransHopMeta, StepMeta> { private static Class<?> PKG = TransPainter.class; // for i18n purposes, needed by Translator2!! public static final String STRING_PARTITIONING_CURRENT_STEP = "PartitioningCurrentStep"; public static final String STRING_REMOTE_INPUT_STEPS = "RemoteInputSteps"; public static final String STRING_REMOTE_OUTPUT_STEPS = "RemoteOutputSteps"; public static final String STRING_STEP_ERROR_LOG = "StepErrorLog"; public static final String STRING_HOP_TYPE_COPY = "HopTypeCopy"; public static final String STRING_ROW_DISTRIBUTION = "RowDistribution"; private TransMeta transMeta; private Map<StepMeta, String> stepLogMap; private List<StepMeta> mouseOverSteps; private StepMeta startHopStep; private Point endHopLocation; private StepMeta endHopStep; private StepMeta noInputStep; private StreamType candidateHopType; private boolean startErrorHopStep; private StepMeta showTargetStreamsStep; private Trans trans; private boolean slowStepIndicatorEnabled; public static final String[] magnificationDescriptions = new String[] { " 200% ", " 150% ", " 100% ", " 75% ", " 50% ", " 25% " }; public TransPainter( GCInterface gc, TransMeta transMeta, Point area, ScrollBarInterface hori, ScrollBarInterface vert, TransHopMeta candidate, Point drop_candidate, Rectangle selrect, List<AreaOwner> areaOwners, List<StepMeta> mouseOverSteps, int iconsize, int linewidth, int gridsize, int shadowSize, boolean antiAliasing, String noteFontName, int noteFontHeight, Trans trans, boolean slowStepIndicatorEnabled ) { super( gc, transMeta, area, hori, vert, drop_candidate, selrect, areaOwners, iconsize, linewidth, gridsize, shadowSize, antiAliasing, noteFontName, noteFontHeight ); this.transMeta = transMeta; this.candidate = candidate; this.mouseOverSteps = mouseOverSteps; this.trans = trans; this.slowStepIndicatorEnabled = slowStepIndicatorEnabled; stepLogMap = null; } public TransPainter( GCInterface gc, TransMeta transMeta, Point area, ScrollBarInterface hori, ScrollBarInterface vert, TransHopMeta candidate, Point drop_candidate, Rectangle selrect, List<AreaOwner> areaOwners, List<StepMeta> mouseOverSteps, int iconsize, int linewidth, int gridsize, int shadowSize, boolean antiAliasing, String noteFontName, int noteFontHeight ) { this( gc, transMeta, area, hori, vert, candidate, drop_candidate, selrect, areaOwners, mouseOverSteps, iconsize, linewidth, gridsize, shadowSize, antiAliasing, noteFontName, noteFontHeight, new Trans( transMeta ), false ); } private static String[] getPeekTitles() { String[] titles = { BaseMessages.getString( PKG, "PeekMetric.Column.Copynr" ), BaseMessages.getString( PKG, "PeekMetric.Column.Read" ), BaseMessages.getString( PKG, "PeekMetric.Column.Written" ), BaseMessages.getString( PKG, "PeekMetric.Column.Input" ), BaseMessages.getString( PKG, "PeekMetric.Column.Output" ), BaseMessages.getString( PKG, "PeekMetric.Column.Updated" ), BaseMessages.getString( PKG, "PeekMetric.Column.Rejected" ), BaseMessages.getString( PKG, "PeekMetric.Column.Errors" ), BaseMessages.getString( PKG, "PeekMetric.Column.Active" ), BaseMessages.getString( PKG, "PeekMetric.Column.Time" ), BaseMessages.getString( PKG, "PeekMetric.Column.Speed" ), BaseMessages.getString( PKG, "PeekMetric.Column.PriorityBufferSizes" ) }; return titles; } public void buildTransformationImage() { Point max = transMeta.getMaximum(); Point thumb = getThumb( area, max ); offset = getOffset( thumb, area ); // First clear the image in the background color gc.setBackground( EColor.BACKGROUND ); gc.fillRectangle( 0, 0, area.x, area.y ); // If there is a shadow, we draw the transformation first with an alpha setting // if ( shadowSize > 0 ) { shadow = true; gc.setTransform( translationX, translationY, shadowSize, magnification ); gc.setAlpha( 20 ); drawTrans( thumb ); } // Draw the transformation onto the image // shadow = false; gc.setTransform( translationX, translationY, 0, magnification ); gc.setAlpha( 255 ); drawTrans( thumb ); gc.dispose(); } private void drawTrans( Point thumb ) { if ( !shadow && gridSize > 1 ) { drawGrid(); } if ( hori != null && vert != null ) { hori.setThumb( thumb.x ); vert.setThumb( thumb.y ); } try { ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransPainterStart.id, this ); } catch ( KettleException e ) { LogChannel.GENERAL.logError( "Error in TransPainterStart extension point", e ); } gc.setFont( EFont.NOTE ); // First the notes for ( int i = 0; i < transMeta.nrNotes(); i++ ) { NotePadMeta ni = transMeta.getNote( i ); drawNote( ni ); } gc.setFont( EFont.GRAPH ); gc.setBackground( EColor.BACKGROUND ); for ( int i = 0; i < transMeta.nrTransHops(); i++ ) { TransHopMeta hi = transMeta.getTransHop( i ); drawHop( hi ); } EImage arrow; if ( candidate != null ) { drawHop( candidate, true ); } else { if ( startHopStep != null && endHopLocation != null ) { Point fr = startHopStep.getLocation(); Point to = endHopLocation; if ( endHopStep == null ) { gc.setForeground( EColor.GRAY ); arrow = EImage.ARROW_DISABLED; } else { gc.setForeground( EColor.BLUE ); arrow = EImage.ARROW_DEFAULT; } Point start = real2screen( fr.x + iconsize / 2, fr.y + iconsize / 2 ); Point end = real2screen( to.x, to.y ); drawArrow( arrow, start.x, start.y, end.x, end.y, theta, calcArrowLength(), 1.2, null, startHopStep, endHopStep == null ? endHopLocation : endHopStep ); } else if ( endHopStep != null && endHopLocation != null ) { Point fr = endHopLocation; Point to = endHopStep.getLocation(); if ( startHopStep == null ) { gc.setForeground( EColor.GRAY ); arrow = EImage.ARROW_DISABLED; } else { gc.setForeground( EColor.BLUE ); arrow = EImage.ARROW_DEFAULT; } Point start = real2screen( fr.x, fr.y ); Point end = real2screen( to.x + iconsize / 2, to.y + iconsize / 2 ); drawArrow( arrow, start.x, start.y, end.x, end.y, theta, calcArrowLength(), 1.2, null, startHopStep == null ? endHopLocation : startHopStep, endHopStep ); } } // Draw regular step appearance for ( int i = 0; i < transMeta.nrSteps(); i++ ) { StepMeta stepMeta = transMeta.getStep( i ); if ( stepMeta.isDrawn() ) { drawStep( stepMeta ); } } if ( slowStepIndicatorEnabled ) { // Highlight possible bottlenecks for ( int i = 0; i < transMeta.nrSteps(); i++ ) { StepMeta stepMeta = transMeta.getStep( i ); if ( stepMeta.isDrawn() ) { checkDrawSlowStepIndicator( stepMeta ); } } } // Draw step status indicators (running vs. done) for ( int i = 0; i < transMeta.nrSteps(); i++ ) { StepMeta stepMeta = transMeta.getStep( i ); if ( stepMeta.isDrawn() ) { drawStepStatusIndicator( stepMeta ); } } // Draw performance table for selected step(s) for ( int i = 0; i < transMeta.nrSteps(); i++ ) { StepMeta stepMeta = transMeta.getStep( i ); if ( stepMeta.isDrawn() ) { drawStepPerformanceTable( stepMeta ); } } int selectedStepsCount = 0; for ( int i = transMeta.nrSteps() - 1; i >= 0; i-- ) { StepMeta stepMeta = transMeta.getStep( i ); if ( stepMeta.isSelected() ) { selectedStepsCount++; } } TransPainterFlyoutExtension extension = null; for ( int i = transMeta.nrSteps() - 1; i >= 0; i-- ) { StepMeta stepMeta = transMeta.getStep( i ); if ( stepMeta.isSelected() && stepMeta.isDrawn() && selectedStepsCount == 1 ) { extension = new TransPainterFlyoutExtension( gc, areaOwners, transMeta, stepMeta, translationX, translationY, magnification, area, offset ); break; } } if ( extension == null ) { // pass null to notify extension that nothing is selected extension = new TransPainterFlyoutExtension( gc, areaOwners, transMeta, null, translationX, translationY, magnification, area, offset ); } try { ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransPainterFlyout.id, extension ); } catch ( Exception e ) { LogChannel.GENERAL.logError( "Error calling extension point(s) for the transformation painter step", e ); } // Display an icon on the indicated location signaling to the user that the step in question does not accept input // if ( noInputStep != null ) { gc.setLineWidth( 2 ); gc.setForeground( EColor.RED ); Point n = noInputStep.getLocation(); gc.drawLine( n.x - 5, n.y - 5, n.x + iconsize + 10, n.y + iconsize + 10 ); gc.drawLine( n.x - 5, n.y + iconsize + 5, n.x + iconsize + 5, n.y - 5 ); } if ( drop_candidate != null ) { gc.setLineStyle( ELineStyle.SOLID ); gc.setForeground( EColor.BLACK ); Point screen = real2screen( drop_candidate.x, drop_candidate.y ); gc.drawRectangle( screen.x, screen.y, iconsize, iconsize ); } try { ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransPainterEnd.id, this ); } catch ( KettleException e ) { LogChannel.GENERAL.logError( "Error in TransPainterEnd extension point", e ); } if ( !shadow ) { drawRect( selrect ); } } private void checkDrawSlowStepIndicator( StepMeta stepMeta ) { if ( stepMeta == null ) { return; } // draw optional performance indicator if ( trans != null ) { Point pt = stepMeta.getLocation(); if ( pt == null ) { pt = new Point( 50, 50 ); } Point screen = real2screen( pt.x, pt.y ); int x = screen.x; int y = screen.y; List<StepInterface> steps = trans.findBaseSteps( stepMeta.getName() ); for ( StepInterface step : steps ) { if ( step.isRunning() ) { int inputRows = step.rowsetInputSize(); int outputRows = step.rowsetOutputSize(); // if the step can't keep up with its input, mark it by drawing an animation boolean isSlow = inputRows * 0.85 > outputRows; if ( isSlow ) { gc.setLineWidth( linewidth + 1 ); if ( System.currentTimeMillis() % 2000 > 1000 ) { gc.setForeground( EColor.BACKGROUND ); gc.setLineStyle( ELineStyle.SOLID ); gc.drawRectangle( x + 1, y + 1, iconsize - 2, iconsize - 2 ); gc.setForeground( EColor.DARKGRAY ); gc.setLineStyle( ELineStyle.DOT ); gc.drawRectangle( x + 1, y + 1, iconsize - 2, iconsize - 2 ); } else { gc.setForeground( EColor.DARKGRAY ); gc.setLineStyle( ELineStyle.SOLID ); gc.drawRectangle( x + 1, y + 1, iconsize - 2, iconsize - 2 ); gc.setForeground( EColor.BACKGROUND ); gc.setLineStyle( ELineStyle.DOT ); gc.drawRectangle( x + 1, y + 1, iconsize - 2, iconsize - 2 ); } } } gc.setLineStyle( ELineStyle.SOLID ); } } } private void drawStepPerformanceTable( StepMeta stepMeta ) { if ( stepMeta == null ) { return; } // draw optional performance indicator if ( trans != null ) { Point pt = stepMeta.getLocation(); if ( pt == null ) { pt = new Point( 50, 50 ); } Point screen = real2screen( pt.x, pt.y ); int x = screen.x; int y = screen.y; List<StepInterface> steps = trans.findBaseSteps( stepMeta.getName() ); // draw mouse over performance indicator if ( trans.isRunning() ) { if ( stepMeta.isSelected() ) { // determine popup dimensions up front int popupX = x; int popupY = y; int popupWidth = 0; int popupHeight = 1; gc.setFont( EFont.SMALL ); Point p = gc.textExtent( "0000000000" ); int colWidth = p.x + MINI_ICON_MARGIN; int rowHeight = p.y + MINI_ICON_MARGIN; int titleWidth = 0; // calculate max title width to get the colum with String[] titles = TransPainter.getPeekTitles(); for ( String title : titles ) { Point titleExtent = gc.textExtent( title ); titleWidth = Math.max( titleExtent.x + MINI_ICON_MARGIN, titleWidth ); popupHeight += titleExtent.y + MINI_ICON_MARGIN; } popupWidth = titleWidth + 2 * MINI_ICON_MARGIN; // determine total popup width popupWidth += steps.size() * colWidth; // determine popup position popupX = popupX + ( iconsize - popupWidth ) / 2; popupY = popupY - popupHeight - MINI_ICON_MARGIN; // draw the frame gc.setForeground( EColor.DARKGRAY ); gc.setBackground( EColor.LIGHTGRAY ); gc.setLineWidth( 1 ); gc.fillRoundRectangle( popupX, popupY, popupWidth, popupHeight, 7, 7 ); // draw the title columns // gc.setBackground(EColor.BACKGROUND); // gc.fillRoundRectangle(popupX, popupY, titleWidth+MINI_ICON_MARGIN, popupHeight, 7, 7); gc.setBackground( EColor.LIGHTGRAY ); gc.drawRoundRectangle( popupX, popupY, popupWidth, popupHeight, 7, 7 ); for ( int i = 0, barY = popupY; i < titles.length; i++ ) { // fill each line with a slightly different background color if ( i % 2 == 1 ) { gc.setBackground( EColor.BACKGROUND ); } else { gc.setBackground( EColor.LIGHTGRAY ); } gc.fillRoundRectangle( popupX + 1, barY + 1, popupWidth - 2, rowHeight, 7, 7 ); barY += rowHeight; } // draw the header column int rowY = popupY + MINI_ICON_MARGIN; int rowX = popupX + MINI_ICON_MARGIN; gc.setForeground( EColor.BLACK ); gc.setBackground( EColor.BACKGROUND ); for ( int i = 0; i < titles.length; i++ ) { if ( i % 2 == 1 ) { gc.setBackground( EColor.BACKGROUND ); } else { gc.setBackground( EColor.LIGHTGRAY ); } gc.drawText( titles[i], rowX, rowY ); rowY += rowHeight; } // draw the values for each copy of the step gc.setBackground( EColor.LIGHTGRAY ); rowX += titleWidth; for ( StepInterface step : steps ) { rowX += colWidth; rowY = popupY + MINI_ICON_MARGIN; StepStatus stepStatus = new StepStatus( step ); String[] fields = stepStatus.getPeekFields(); for ( int i = 0; i < fields.length; i++ ) { if ( i % 2 == 1 ) { gc.setBackground( EColor.BACKGROUND ); } else { gc.setBackground( EColor.LIGHTGRAY ); } drawTextRightAligned( fields[i], rowX, rowY ); rowY += rowHeight; } } } } } } private void drawStepStatusIndicator( StepMeta stepMeta ) { if ( stepMeta == null ) { return; } // draw status indicator if ( trans != null ) { Point pt = stepMeta.getLocation(); if ( pt == null ) { pt = new Point( 50, 50 ); } Point screen = real2screen( pt.x, pt.y ); int x = screen.x; int y = screen.y; List<StepInterface> steps = trans.findBaseSteps( stepMeta.getName() ); for ( StepInterface step : steps ) { if ( step.getStatus().equals( StepExecutionStatus.STATUS_FINISHED ) ) { gc.drawImage( EImage.TRUE, ( x + iconsize ) - ( MINI_ICON_SIZE / 2 ) + 4, y - ( MINI_ICON_SIZE / 2 ) - 1, magnification ); } } } } private void drawTextRightAligned( String txt, int x, int y ) { int off = gc.textExtent( txt ).x; x -= off; gc.drawText( txt, x, y ); } private void drawHop( TransHopMeta hi ) { drawHop( hi, false ); } private void drawHop( TransHopMeta hi, boolean isCandidate ) { StepMeta fs = hi.getFromStep(); StepMeta ts = hi.getToStep(); if ( fs != null && ts != null ) { drawLine( fs, ts, hi, isCandidate ); } } private void drawStep( StepMeta stepMeta ) { if ( stepMeta == null ) { return; } int alpha = gc.getAlpha(); StepIOMetaInterface ioMeta = stepMeta.getStepMetaInterface().getStepIOMeta(); Point pt = stepMeta.getLocation(); if ( pt == null ) { pt = new Point( 50, 50 ); } Point screen = real2screen( pt.x, pt.y ); int x = screen.x; int y = screen.y; boolean stepError = false; if ( stepLogMap != null && !stepLogMap.isEmpty() ) { String log = stepLogMap.get( stepMeta ); if ( !Utils.isEmpty( log ) ) { stepError = true; } } // REMOTE STEPS // First draw an extra indicator for remote input steps... // if ( !stepMeta.getRemoteInputSteps().isEmpty() ) { gc.setLineWidth( 1 ); gc.setForeground( EColor.GRAY ); gc.setBackground( EColor.BACKGROUND ); gc.setFont( EFont.GRAPH ); String nrInput = Integer.toString( stepMeta.getRemoteInputSteps().size() ); Point textExtent = gc.textExtent( nrInput ); textExtent.x += 2; // add a tiny listartHopStepttle bit of a margin textExtent.y += 2; // Draw it an icon above the step icon. // Draw it an icon and a half to the left // Point point = new Point( x - iconsize - iconsize / 2, y - iconsize ); gc.drawRectangle( point.x, point.y, textExtent.x, textExtent.y ); gc.drawText( nrInput, point.x + 1, point.y + 1 ); // Now we draw an arrow from the cube to the step... // gc.drawLine( point.x + textExtent.x, point.y + textExtent.y / 2, x - iconsize / 2, point.y + textExtent.y / 2 ); drawArrow( EImage.ARROW_DISABLED, x - iconsize / 2, point.y + textExtent.y / 2, x + iconsize / 3, y, Math.toRadians( 15 ), 15, 1.8, null, null, null ); // Add to the list of areas... if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.REMOTE_INPUT_STEP, point.x, point.y, textExtent.x, textExtent.y, offset, stepMeta, STRING_REMOTE_INPUT_STEPS ) ); } } // Then draw an extra indicator for remote output steps... // if ( !stepMeta.getRemoteOutputSteps().isEmpty() ) { gc.setLineWidth( 1 ); gc.setForeground( EColor.GRAY ); gc.setBackground( EColor.BACKGROUND ); gc.setFont( EFont.GRAPH ); String nrOutput = Integer.toString( stepMeta.getRemoteOutputSteps().size() ); Point textExtent = gc.textExtent( nrOutput ); textExtent.x += 2; // add a tiny little bit of a margin textExtent.y += 2; // Draw it an icon above the step icon. // Draw it an icon and a half to the right // Point point = new Point( x + 2 * iconsize + iconsize / 2 - textExtent.x, y - iconsize ); gc.drawRectangle( point.x, point.y, textExtent.x, textExtent.y ); gc.drawText( nrOutput, point.x + 1, point.y + 1 ); // Now we draw an arrow from the cube to the step... // This time, we start at the left side... // gc.drawLine( point.x, point.y + textExtent.y / 2, x + iconsize + iconsize / 2, point.y + textExtent.y / 2 ); drawArrow( EImage.ARROW_DISABLED, x + 2 * iconsize / 3, y, x + iconsize + iconsize / 2, point.y + textExtent.y / 2, Math .toRadians( 15 ), 15, 1.8, null, null, null ); // Add to the list of areas... if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.REMOTE_OUTPUT_STEP, point.x, point.y, textExtent.x, textExtent.y, offset, stepMeta, STRING_REMOTE_OUTPUT_STEPS ) ); } } // PARTITIONING // If this step is partitioned, we're drawing a small symbol indicating this... // if ( stepMeta.isPartitioned() ) { gc.setLineWidth( 1 ); gc.setForeground( EColor.RED ); gc.setBackground( EColor.BACKGROUND ); gc.setFont( EFont.GRAPH ); PartitionSchema partitionSchema = stepMeta.getStepPartitioningMeta().getPartitionSchema(); if ( partitionSchema != null ) { String nrInput; if ( partitionSchema.isDynamicallyDefined() ) { nrInput = "Dx" + partitionSchema.getNumberOfPartitionsPerSlave(); } else { nrInput = "Px" + Integer.toString( partitionSchema.getPartitionIDs().size() ); } Point textExtent = gc.textExtent( nrInput ); textExtent.x += 2; // add a tiny little bit of a margin textExtent.y += 2; // Draw it a 2 icons above the step icon. // Draw it an icon and a half to the left // Point point = new Point( x - iconsize - iconsize / 2, y - iconsize - iconsize ); gc.drawRectangle( point.x, point.y, textExtent.x, textExtent.y ); gc.drawText( nrInput, point.x + 1, point.y + 1 ); // Now we draw an arrow from the cube to the step... // gc.drawLine( point.x + textExtent.x, point.y + textExtent.y / 2, x - iconsize / 2, point.y + textExtent.y / 2 ); gc.drawLine( x - iconsize / 2, point.y + textExtent.y / 2, x + iconsize / 3, y ); // Also draw the name of the partition schema below the box // gc.setForeground( EColor.GRAY ); gc.drawText( Const.NVL( partitionSchema.getName(), "<no partition name>" ), point.x, point.y + textExtent.y + 3, true ); // Add to the list of areas... // if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.STEP_PARTITIONING, point.x, point.y, textExtent.x, textExtent.y, offset, stepMeta, STRING_PARTITIONING_CURRENT_STEP ) ); } } } String name = stepMeta.getName(); if ( stepMeta.isSelected() ) { gc.setLineWidth( linewidth + 2 ); } else { gc.setLineWidth( linewidth ); } // Add to the list of areas... if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.STEP_ICON, x, y, iconsize, iconsize, offset, transMeta, stepMeta ) ); } gc.setBackground( EColor.BACKGROUND ); gc.fillRoundRectangle( x - 1, y - 1, iconsize + 1, iconsize + 1, 8, 8 ); gc.drawStepIcon( x, y, stepMeta, magnification ); if ( stepError || stepMeta.isMissing() ) { gc.setForeground( EColor.RED ); } else { gc.setForeground( EColor.CRYSTAL ); } if ( stepMeta.isSelected() ) { gc.setForeground( 0, 93, 166 ); } gc.drawRoundRectangle( x - 1, y - 1, iconsize + 1, iconsize + 1, 8, 8 ); Point namePosition = getNamePosition( name, screen, iconsize ); if ( stepMeta.isSelected() ) { int tmpAlpha = gc.getAlpha(); gc.setAlpha( 192 ); gc.setBackground( 216, 230, 241 ); gc.fillRoundRectangle( namePosition.x - 8, namePosition.y - 2, gc.textExtent( name ).x + 15, 25, BasePainter.CORNER_RADIUS_5 + 15, BasePainter.CORNER_RADIUS_5 + 15 ); gc.setAlpha( tmpAlpha ); } gc.setForeground( EColor.BLACK ); gc.setFont( EFont.GRAPH ); gc.drawText( name, namePosition.x, namePosition.y + 2, true ); boolean partitioned = false; StepPartitioningMeta meta = stepMeta.getStepPartitioningMeta(); if ( stepMeta.isPartitioned() && meta != null ) { partitioned = true; } String clusterMessage = ""; if ( stepMeta.getClusterSchema() != null ) { clusterMessage = "C"; if ( stepMeta.getClusterSchema().isDynamic() ) { clusterMessage += "xN"; } else { clusterMessage += "x" + stepMeta.getClusterSchema().findNrSlaves(); } Point textExtent = gc.textExtent( clusterMessage ); gc.setBackground( EColor.BACKGROUND ); gc.setForeground( EColor.BLACK ); gc.drawText( clusterMessage, x - textExtent.x + 1, y - textExtent.y + 1 ); } if ( stepMeta.getCopies() != 1 && !partitioned ) { gc.setBackground( EColor.BACKGROUND ); gc.setForeground( EColor.BLACK ); String copies = "x" + stepMeta.getCopiesString(); Point textExtent = gc.textExtent( copies ); if ( stepMeta.getClusterSchema() != null ) { Point clusterTextExtent = gc.textExtent( clusterMessage ); gc.drawText( copies, x - textExtent.x + 1, y - textExtent.y - clusterTextExtent.y + 1, false ); areaOwners.add( new AreaOwner( AreaType.STEP_COPIES_TEXT, x - textExtent.x + 1, y - textExtent.y - clusterTextExtent.y + 1, textExtent.x, textExtent.y, offset, transMeta, stepMeta ) ); } else { gc.drawText( copies, x - textExtent.x + 1, y - textExtent.y + 1, false ); areaOwners.add( new AreaOwner( AreaType.STEP_COPIES_TEXT, x - textExtent.x + 1, y - textExtent.y + 1, textExtent.x, textExtent.y, offset, transMeta, stepMeta ) ); } } // If there was an error during the run, the map "stepLogMap" is not empty and not null. // if ( stepError ) { String log = stepLogMap.get( stepMeta ); // Show an error lines icon in the upper right corner of the step... // int xError = ( x + iconsize ) - ( MINI_ICON_SIZE / 2 ) + 4; int yError = y - ( MINI_ICON_SIZE / 2 ) - 1; Point ib = gc.getImageBounds( EImage.STEP_ERROR_RED ); gc.drawImage( EImage.STEP_ERROR_RED, xError, yError, magnification ); if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.STEP_ERROR_RED_ICON, pt.x + iconsize - 3, pt.y - 8, ib.x, ib.y, offset, log, STRING_STEP_ERROR_LOG ) ); } } // Optionally drawn the mouse-over information // if ( mouseOverSteps.contains( stepMeta ) ) { gc.setTransform( translationX, translationY, 0, BasePainter.FACTOR_1_TO_1 ); StepMetaInterface stepMetaInterface = stepMeta.getStepMetaInterface(); boolean mdiSupport = stepMetaInterface.getStepMetaInjectionInterface() != null || BeanInjectionInfo.isInjectionSupported( stepMetaInterface.getClass() ); EImage[] miniIcons; if ( mdiSupport ) { miniIcons = new EImage[] { EImage.INPUT, EImage.EDIT, EImage.CONTEXT_MENU, EImage.OUTPUT, EImage.INJECT, }; } else { miniIcons = new EImage[] { EImage.INPUT, EImage.EDIT, EImage.CONTEXT_MENU, EImage.OUTPUT, }; } int totalHeight = 0; int totalIconsWidth = 0; int totalWidth = 2 * MINI_ICON_MARGIN; for ( EImage miniIcon : miniIcons ) { Point bounds = gc.getImageBounds( miniIcon ); totalWidth += bounds.x + MINI_ICON_MARGIN; totalIconsWidth += bounds.x + MINI_ICON_MARGIN; if ( bounds.y > totalHeight ) { totalHeight = bounds.y; } } totalHeight += 2 * MINI_ICON_MARGIN; gc.setFont( EFont.SMALL ); String trimmedName = stepMeta.getName().length() < 30 ? stepMeta.getName() : stepMeta.getName().substring( 0, 30 ); Point nameExtent = gc.textExtent( trimmedName ); nameExtent.y += 2 * MINI_ICON_MARGIN; nameExtent.x += 3 * MINI_ICON_MARGIN; totalHeight += nameExtent.y; if ( nameExtent.x > totalWidth ) { totalWidth = nameExtent.x; } int areaX = translateToCurrentScale( x ) + translateToCurrentScale( iconsize ) / 2 - totalWidth / 2 + MINI_ICON_SKEW; int areaY = translateToCurrentScale( y ) + translateToCurrentScale( iconsize ) + MINI_ICON_DISTANCE + BasePainter.CONTENT_MENU_INDENT; gc.setForeground( EColor.CRYSTAL ); gc.setBackground( EColor.CRYSTAL ); gc.setLineWidth( 1 ); gc.fillRoundRectangle( areaX, areaY, totalWidth, totalHeight, BasePainter.CORNER_RADIUS_5, BasePainter.CORNER_RADIUS_5 ); gc.setBackground( EColor.WHITE ); gc.fillRoundRectangle( areaX, areaY + nameExtent.y, totalWidth, ( totalHeight - nameExtent.y ), BasePainter.CORNER_RADIUS_5, BasePainter.CORNER_RADIUS_5 ); gc.fillRectangle( areaX, areaY + nameExtent.y, totalWidth, ( totalHeight - nameExtent.y ) / 2 ); gc.drawRoundRectangle( areaX, areaY, totalWidth, totalHeight, BasePainter.CORNER_RADIUS_5, BasePainter.CORNER_RADIUS_5 ); gc.setForeground( EColor.WHITE ); gc.drawText( trimmedName, areaX + ( totalWidth - nameExtent.x ) / 2 + MINI_ICON_MARGIN, areaY + MINI_ICON_MARGIN, true ); gc.setForeground( EColor.CRYSTAL ); gc.setBackground( EColor.CRYSTAL ); gc.setFont( EFont.GRAPH ); areaOwners.add( new AreaOwner( AreaType.MINI_ICONS_BALLOON, translateTo1To1( areaX ), translateTo1To1( areaY ), translateTo1To1( totalWidth ), translateTo1To1( totalHeight ), offset, stepMeta, ioMeta ) ); gc.fillPolygon( new int[] { areaX + totalWidth / 2 - MINI_ICON_TRIANGLE_BASE / 2 + 1, areaY + 2, areaX + totalWidth / 2 + MINI_ICON_TRIANGLE_BASE / 2, areaY + 2, areaX + totalWidth / 2 - MINI_ICON_SKEW, areaY - MINI_ICON_DISTANCE - 3, } ); gc.setBackground( EColor.WHITE ); // Put on the icons... // int xIcon = areaX + ( totalWidth - totalIconsWidth ) / 2 + MINI_ICON_MARGIN; int yIcon = areaY + 5 + nameExtent.y; for ( int i = 0; i < miniIcons.length; i++ ) { EImage miniIcon = miniIcons[i]; Point bounds = gc.getImageBounds( miniIcon ); boolean enabled = false; switch ( i ) { case 0: // INPUT enabled = ioMeta.isInputAcceptor() || ioMeta.isInputDynamic(); areaOwners.add( new AreaOwner( AreaType.STEP_INPUT_HOP_ICON, translateTo1To1( xIcon ), translateTo1To1( yIcon ), translateTo1To1( bounds.x ), translateTo1To1( bounds.y ), offset, stepMeta, ioMeta ) ); break; case 1: // EDIT enabled = true; areaOwners.add( new AreaOwner( AreaType.STEP_EDIT_ICON, translateTo1To1( xIcon ), translateTo1To1( yIcon ), translateTo1To1( bounds.x ), translateTo1To1( bounds.y ), offset, stepMeta, ioMeta ) ); break; case 2: // STEP_MENU enabled = true; areaOwners.add( new AreaOwner( AreaType.STEP_MENU_ICON, translateTo1To1( xIcon ), translateTo1To1( yIcon ), translateTo1To1( bounds.x ), translateTo1To1( bounds.y ), offset, stepMeta, ioMeta ) ); break; case 3: // OUTPUT enabled = ioMeta.isOutputProducer() || ioMeta.isOutputDynamic(); areaOwners.add( new AreaOwner( AreaType.STEP_OUTPUT_HOP_ICON, translateTo1To1( xIcon ), translateTo1To1( yIcon ), translateTo1To1( bounds.x ), translateTo1To1( bounds.y ), offset, stepMeta, ioMeta ) ); break; case 4: // INJECT enabled = mdiSupport; StepMetaInterface mdiObject = mdiSupport ? stepMetaInterface : null; areaOwners.add( new AreaOwner( AreaType.STEP_INJECT_ICON, translateTo1To1( xIcon ), translateTo1To1( yIcon ), translateTo1To1( bounds.x ), translateTo1To1( bounds.y ), offset, stepMeta, mdiObject ) ); break; default: break; } if ( enabled ) { gc.setAlpha( 255 ); } else { gc.setAlpha( 100 ); } gc.drawImage( miniIcon, xIcon, yIcon, BasePainter.FACTOR_1_TO_1 ); xIcon += bounds.x + 5; } // OK, see if we need to show a slide-out for target streams... // if ( showTargetStreamsStep != null ) { ioMeta = showTargetStreamsStep.getStepMetaInterface().getStepIOMeta(); List<StreamInterface> targetStreams = ioMeta.getTargetStreams(); int targetsWidth = 0; int targetsHeight = 0; for ( int i = 0; i < targetStreams.size(); i++ ) { String description = targetStreams.get( i ).getDescription(); Point extent = gc.textExtent( description ); if ( extent.x > targetsWidth ) { targetsWidth = extent.x; } targetsHeight += extent.y + MINI_ICON_MARGIN; } targetsWidth += MINI_ICON_MARGIN; gc.setBackground( EColor.LIGHTGRAY ); gc.fillRoundRectangle( areaX, areaY + totalHeight + 2, targetsWidth, targetsHeight, 7, 7 ); gc.drawRoundRectangle( areaX, areaY + totalHeight + 2, targetsWidth, targetsHeight, 7, 7 ); int targetY = areaY + totalHeight + MINI_ICON_MARGIN; for ( int i = 0; i < targetStreams.size(); i++ ) { String description = targetStreams.get( i ).getDescription(); Point extent = gc.textExtent( description ); gc.drawText( description, areaX + MINI_ICON_MARGIN, targetY, true ); if ( i < targetStreams.size() - 1 ) { gc.drawLine( areaX + MINI_ICON_MARGIN / 2, targetY + extent.y + 3, areaX + targetsWidth - MINI_ICON_MARGIN / 2, targetY + extent.y + 2 ); } areaOwners.add( new AreaOwner( AreaType.STEP_TARGET_HOP_ICON_OPTION, areaX, targetY, targetsWidth, extent.y + MINI_ICON_MARGIN, offset, stepMeta, targetStreams.get( i ) ) ); targetY += extent.y + MINI_ICON_MARGIN; } gc.setBackground( EColor.BACKGROUND ); } gc.setTransform( translationX, translationY, 0, magnification ); } TransPainterExtension extension = new TransPainterExtension( gc, shadow, areaOwners, transMeta, stepMeta, null, x, y, 0, 0, 0, 0, offset, iconsize ); try { ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransPainterStep.id, extension ); } catch ( Exception e ) { LogChannel.GENERAL.logError( "Error calling extension point(s) for the transformation painter step", e ); } // Restore the previous alpha value // gc.setAlpha( alpha ); } public Point getNamePosition( String string, Point screen, int iconsize ) { Point textsize = gc.textExtent( string ); int xpos = screen.x + ( iconsize / 2 ) - ( textsize.x / 2 ); int ypos = screen.y + iconsize + 5; return new Point( xpos, ypos ); } private void drawLine( StepMeta fs, StepMeta ts, TransHopMeta hi, boolean is_candidate ) { int[] line = getLine( fs, ts ); EColor col; ELineStyle linestyle = ELineStyle.SOLID; int activeLinewidth = linewidth; EImage arrow; if ( is_candidate ) { col = EColor.BLUE; arrow = EImage.ARROW_CANDIDATE; } else { if ( hi.isEnabled() ) { if ( fs.isSendingErrorRowsToStep( ts ) ) { col = EColor.RED; linestyle = ELineStyle.DASH; activeLinewidth = linewidth + 1; arrow = EImage.ARROW_ERROR; } else { col = EColor.HOP_DEFAULT; arrow = EImage.ARROW_DEFAULT; } } else { col = EColor.GRAY; arrow = EImage.ARROW_DISABLED; } } if ( hi.split ) { activeLinewidth = linewidth + 2; } // Check to see if the source step is an info step for the target step. // StepIOMetaInterface ioMeta = ts.getStepMetaInterface().getStepIOMeta(); List<StreamInterface> infoStreams = ioMeta.getInfoStreams(); if ( !infoStreams.isEmpty() ) { // Check this situation, the source step can't run in multiple copies! // for ( StreamInterface stream : infoStreams ) { if ( fs.getName().equalsIgnoreCase( stream.getStepname() ) ) { // This is the info step over this hop! // if ( fs.getCopies() > 1 ) { // This is not a desirable situation, it will always end in error. // As such, it's better not to give feedback on it. // We do this by drawing an error icon over the hop... // col = EColor.RED; arrow = EImage.ARROW_ERROR; } } } } gc.setForeground( col ); gc.setLineStyle( linestyle ); gc.setLineWidth( activeLinewidth ); drawArrow( arrow, line, hi, fs, ts ); if ( hi.split ) { gc.setLineWidth( linewidth ); } gc.setForeground( EColor.BLACK ); gc.setBackground( EColor.BACKGROUND ); gc.setLineStyle( ELineStyle.SOLID ); } @Override protected void drawArrow( EImage arrow, int x1, int y1, int x2, int y2, double theta, int size, double factor, TransHopMeta transHop, Object startObject, Object endObject ) { int mx, my; int a, b, dist; double angle; gc.drawLine( x1, y1, x2, y2 ); // in between 2 points mx = x1 + ( x2 - x1 ) / 2; my = y1 + ( y2 - y1 ) / 2; a = Math.abs( x2 - x1 ); b = Math.abs( y2 - y1 ); dist = (int) Math.sqrt( a * a + b * b ); // determine factor (position of arrow to left side or right side // 0-->100%) if ( factor < 0 ) { if ( dist >= 2 * iconsize ) { factor = 1.3; } else { factor = 1.2; } } // in between 2 points mx = (int) ( x1 + factor * ( x2 - x1 ) / 2 ); my = (int) ( y1 + factor * ( y2 - y1 ) / 2 ); // calculate points for arrowhead // calculate points for arrowhead angle = Math.atan2( y2 - y1, x2 - x1 ) + ( Math.PI / 2 ); boolean q1 = Math.toDegrees( angle ) >= 0 && Math.toDegrees( angle ) <= 90; boolean q2 = Math.toDegrees( angle ) > 90 && Math.toDegrees( angle ) <= 180; boolean q3 = Math.toDegrees( angle ) > 180 && Math.toDegrees( angle ) <= 270; boolean q4 = Math.toDegrees( angle ) > 270 || Math.toDegrees( angle ) < 0; if ( q1 || q3 ) { gc.drawImage( arrow, mx + 1, my, magnification, angle ); } else if ( q2 || q4 ) { gc.drawImage( arrow, mx, my, magnification, angle ); } if ( startObject instanceof StepMeta && endObject instanceof StepMeta ) { factor = 0.8; StepMeta fs = (StepMeta) startObject; StepMeta ts = (StepMeta) endObject; // in between 2 points mx = (int) ( x1 + factor * ( x2 - x1 ) / 2 ) - 8; my = (int) ( y1 + factor * ( y2 - y1 ) / 2 ) - 8; boolean errorHop = fs.isSendingErrorRowsToStep( ts ) || ( startErrorHopStep && fs.equals( startHopStep ) ); boolean targetHop = Const.indexOfString( ts.getName(), fs.getStepMetaInterface().getStepIOMeta().getTargetStepnames() ) >= 0; if ( targetHop ) { StepIOMetaInterface ioMeta = fs.getStepMetaInterface().getStepIOMeta(); StreamInterface targetStream = ioMeta.findTargetStream( ts ); if ( targetStream != null ) { EImage hopsIcon = BasePainter.getStreamIconImage( targetStream.getStreamIcon() ); Point bounds = gc.getImageBounds( hopsIcon ); gc.drawImage( hopsIcon, mx, my, magnification ); if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.STEP_TARGET_HOP_ICON, mx, my, bounds.x, bounds.y, offset, fs, targetStream ) ); } } } else if ( fs.isDistributes() && fs.getRowDistribution() != null && !ts.getStepPartitioningMeta().isMethodMirror() && !errorHop ) { // Draw the custom row distribution plugin icon // EImage eImage = fs.getRowDistribution().getDistributionImage(); if ( eImage != null ) { Point bounds = gc.getImageBounds( eImage ); gc.drawImage( eImage, mx, my, magnification ); if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.ROW_DISTRIBUTION_ICON, mx, my, bounds.x, bounds.y, offset, fs, STRING_ROW_DISTRIBUTION ) ); } mx += 16; } } else if ( !fs.isDistributes() && !ts.getStepPartitioningMeta().isMethodMirror() && !errorHop ) { // Draw the copy icon on the hop // Point bounds = gc.getImageBounds( EImage.COPY_ROWS ); gc.drawImage( EImage.COPY_ROWS, mx, my, magnification ); if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.HOP_COPY_ICON, mx, my, bounds.x, bounds.y, offset, fs, STRING_HOP_TYPE_COPY ) ); } mx += 16; } if ( errorHop ) { Point bounds = gc.getImageBounds( EImage.COPY_ROWS ); gc.drawImage( EImage.FALSE, mx, my, magnification ); if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.HOP_ERROR_ICON, mx, my, bounds.x, bounds.y, offset, fs, ts ) ); } mx += 16; } StepIOMetaInterface ioMeta = ts.getStepMetaInterface().getStepIOMeta(); String[] infoStepnames = ioMeta.getInfoStepnames(); if ( ( candidateHopType == StreamType.INFO && ts.equals( endHopStep ) && fs.equals( startHopStep ) ) || Const.indexOfString( fs.getName(), infoStepnames ) >= 0 ) { Point bounds = gc.getImageBounds( EImage.INFO ); gc.drawImage( EImage.INFO, mx, my, magnification ); if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.HOP_INFO_ICON, mx, my, bounds.x, bounds.y, offset, fs, ts ) ); } mx += 16; } // Check to see if the source step is an info step for the target step. // if ( !Utils.isEmpty( infoStepnames ) ) { // Check this situation, the source step can't run in multiple copies! // for ( String infoStep : infoStepnames ) { if ( fs.getName().equalsIgnoreCase( infoStep ) ) { // This is the info step over this hop! // if ( fs.getCopies() > 1 ) { // This is not a desirable situation, it will always end in error. // As such, it's better not to give feedback on it. // We do this by drawing an error icon over the hop... // gc.drawImage( EImage.ERROR, mx, my, magnification ); if ( !shadow ) { areaOwners.add( new AreaOwner( AreaType.HOP_INFO_STEP_COPIES_ERROR, mx, my, MINI_ICON_SIZE, MINI_ICON_SIZE, offset, fs, ts ) ); } mx += 16; } } } } } TransPainterExtension extension = new TransPainterExtension( gc, shadow, areaOwners, transMeta, null, transHop, x1, y1, x2, y2, mx, my, offset, iconsize ); try { ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransPainterArrow.id, extension ); } catch ( Exception e ) { LogChannel.GENERAL.logError( "Error calling extension point(s) for the transformation painter arrow", e ); } } /** * @return the stepLogMap */ public Map<StepMeta, String> getStepLogMap() { return stepLogMap; } /** * @param stepLogMap * the stepLogMap to set */ public void setStepLogMap( Map<StepMeta, String> stepLogMap ) { this.stepLogMap = stepLogMap; } /** * @param startHopStep * the startHopStep to set */ public void setStartHopStep( StepMeta startHopStep ) { this.startHopStep = startHopStep; } /** * @param endHopLocation * the endHopLocation to set */ public void setEndHopLocation( Point endHopLocation ) { this.endHopLocation = endHopLocation; } /** * @param noInputStep * the noInputStep to set */ public void setNoInputStep( StepMeta noInputStep ) { this.noInputStep = noInputStep; } /** * @param endHopStep * the endHopStep to set */ public void setEndHopStep( StepMeta endHopStep ) { this.endHopStep = endHopStep; } public void setCandidateHopType( StreamType candidateHopType ) { this.candidateHopType = candidateHopType; } public void setStartErrorHopStep( boolean startErrorHopStep ) { this.startErrorHopStep = startErrorHopStep; } /** * @return the showTargetStreamsStep */ public StepMeta getShowTargetStreamsStep() { return showTargetStreamsStep; } /** * @param showTargetStreamsStep * the showTargetStreamsStep to set */ public void setShowTargetStreamsStep( StepMeta showTargetStreamsStep ) { this.showTargetStreamsStep = showTargetStreamsStep; } public TransMeta getTransMeta() { return transMeta; } public void setTransMeta( TransMeta transMeta ) { this.transMeta = transMeta; } public List<StepMeta> getMouseOverSteps() { return mouseOverSteps; } public void setMouseOverSteps( List<StepMeta> mouseOverSteps ) { this.mouseOverSteps = mouseOverSteps; } public Trans getTrans() { return trans; } public void setTrans( Trans trans ) { this.trans = trans; } public boolean isSlowStepIndicatorEnabled() { return slowStepIndicatorEnabled; } public void setSlowStepIndicatorEnabled( boolean slowStepIndicatorEnabled ) { this.slowStepIndicatorEnabled = slowStepIndicatorEnabled; } public StepMeta getStartHopStep() { return startHopStep; } public Point getEndHopLocation() { return endHopLocation; } public StepMeta getEndHopStep() { return endHopStep; } public StepMeta getNoInputStep() { return noInputStep; } public StreamType getCandidateHopType() { return candidateHopType; } public boolean isStartErrorHopStep() { return startErrorHopStep; } }