/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.io.process; import java.awt.geom.Rectangle2D; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import com.rapidminer.gui.flow.processrendering.annotations.AnnotationDrawUtils; import com.rapidminer.gui.flow.processrendering.annotations.model.OperatorAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.ProcessAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotations; import com.rapidminer.gui.flow.processrendering.annotations.style.AnnotationAlignment; import com.rapidminer.gui.flow.processrendering.annotations.style.AnnotationColor; import com.rapidminer.gui.flow.processrendering.annotations.style.AnnotationStyle; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.Operator; import com.rapidminer.operator.ProcessRootOperator; /** * {@link ProcessXMLFilter} to handle operator & process annotations data. * * @author Michael Knopf, Marco Boeck, Nils Woehler * @since 7.2.0 */ public class AnnotationProcessXMLFilter implements ProcessXMLFilter { /** user data key for operator annotations */ public static final String KEY_OPERATOR_ANNOTATION = "com.rapidminer.io.process.operator_annotation"; /** user data key for process annotations */ public static final String KEY_PROCESS_ANNOTATION = "com.rapidminer.io.process.process_annotation"; private static final String XML_ATTRIBUTE_HEIGHT = "height"; private static final String XML_ATTRIBUTE_WIDTH = "width"; private static final String XML_ATTRIBUTE_X_POSITION = "x"; private static final String XML_ATTRIBUTE_Y_POSITION = "y"; private static final String XML_TAG_ANNOTATION = "description"; private static final String XML_ATTRIBUTE_COLOR = "color"; private static final String XML_ATTRIBUTE_ALIGNMENT = "align"; private static final String XML_ATTRIBUTE_RESIZED = "resized"; private static final String XML_ATTRIBUTE_COLORED = "colored"; @Override public void operatorExported(final Operator op, final Element opElement) { // add workflow annotations Rectangle2D bounds = ProcessLayoutXMLFilter.lookupOperatorRectangle(op); WorkflowAnnotations annotations = lookupOperatorAnnotations(op); if (annotations != null) { for (WorkflowAnnotation annotation : annotations.getAnnotationsDrawOrder()) { Element annotationElement = opElement.getOwnerDocument().createElement(XML_TAG_ANNOTATION); Text commentNode = opElement.getOwnerDocument().createTextNode(annotation.getComment()); bounds = annotation.getLocation().getBounds(); annotationElement.setAttribute(XML_ATTRIBUTE_WIDTH, "" + (int) bounds.getWidth()); annotationElement.setAttribute(XML_ATTRIBUTE_COLOR, annotation.getStyle().getAnnotationColor().getKey()); annotationElement.setAttribute(XML_ATTRIBUTE_ALIGNMENT, annotation.getStyle().getAnnotationAlignment().getKey()); annotationElement.setAttribute(XML_ATTRIBUTE_COLORED, String.valueOf(annotation.wasColored())); annotationElement.appendChild(commentNode); opElement.appendChild(annotationElement); } } } @Override public void executionUnitExported(final ExecutionUnit process, final Element element) { // add workflow annotations WorkflowAnnotations annotations = lookupProcessAnnotations(process); if (annotations != null) { for (WorkflowAnnotation annotation : annotations.getAnnotationsDrawOrder()) { Element annotationElement = element.getOwnerDocument().createElement(XML_TAG_ANNOTATION); Text commentNode = element.getOwnerDocument().createTextNode(annotation.getComment()); Rectangle2D bounds = annotation.getLocation().getBounds(); annotationElement.setAttribute(XML_ATTRIBUTE_X_POSITION, "" + (int) bounds.getX()); annotationElement.setAttribute(XML_ATTRIBUTE_Y_POSITION, "" + (int) bounds.getY()); annotationElement.setAttribute(XML_ATTRIBUTE_WIDTH, "" + (int) bounds.getWidth()); annotationElement.setAttribute(XML_ATTRIBUTE_HEIGHT, "" + (int) bounds.getHeight()); annotationElement.setAttribute(XML_ATTRIBUTE_COLOR, annotation.getStyle().getAnnotationColor().getKey()); annotationElement.setAttribute(XML_ATTRIBUTE_ALIGNMENT, annotation.getStyle().getAnnotationAlignment().getKey()); annotationElement.setAttribute(XML_ATTRIBUTE_RESIZED, String.valueOf(annotation.wasResized())); annotationElement.setAttribute(XML_ATTRIBUTE_COLORED, String.valueOf(annotation.wasColored())); annotationElement.appendChild(commentNode); element.appendChild(annotationElement); } } } @Override public void operatorImported(final Operator op, final Element opElement) { String x = opElement.getAttribute(XML_ATTRIBUTE_X_POSITION); String y = opElement.getAttribute(XML_ATTRIBUTE_Y_POSITION); String w = opElement.getAttribute(XML_ATTRIBUTE_WIDTH); // workflow annotations NodeList children = opElement.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (child instanceof Element) { Element annotationElem = (Element) child; if (XML_TAG_ANNOTATION.equals(annotationElem.getTagName())) { Node textNode = annotationElem.getChildNodes().item(0); String comment = textNode != null ? textNode.getNodeValue() : ""; String wStr = annotationElem.getAttribute(XML_ATTRIBUTE_WIDTH); String colorStr = annotationElem.getAttribute(XML_ATTRIBUTE_COLOR); String alignStr = annotationElem.getAttribute(XML_ATTRIBUTE_ALIGNMENT); String coloredStr = annotationElem.getAttribute(XML_ATTRIBUTE_COLORED); AnnotationStyle style = new AnnotationStyle(AnnotationColor.fromKey(colorStr), AnnotationAlignment.fromKey(alignStr)); if (op instanceof ProcessRootOperator) { // handle prior 6.4.0 root comments and convert to process annotation ExecutionUnit process = ((ProcessRootOperator) op).getSubprocess(0); // we have no idea (and cannot guess reliably) at size of process // so position legacy comment in top left corner double xCoord = 25; double yCoord = 25; int newWidth = 400; int newHeight = AnnotationDrawUtils .getContentHeight(AnnotationDrawUtils.createStyledCommentString(comment, style), newWidth); boolean overflowing = false; if (newHeight > 500) { newWidth = 1000; newHeight = AnnotationDrawUtils.getContentHeight( AnnotationDrawUtils.createStyledCommentString(comment, style), newWidth); if (newHeight > ProcessAnnotation.MAX_HEIGHT) { newHeight = ProcessAnnotation.MAX_HEIGHT; overflowing = true; } } ProcessAnnotation annotation = new ProcessAnnotation(comment, style, process, false, false, new Rectangle2D.Double(xCoord, yCoord, newWidth, newHeight)); annotation.setOverflowing(overflowing); addProcessAnnotation(annotation); } else { try { double width = Double.parseDouble(wStr); if (width > OperatorAnnotation.DEFAULT_WIDTH) { width = OperatorAnnotation.DEFAULT_WIDTH; } int height = AnnotationDrawUtils.getContentHeight( AnnotationDrawUtils.createStyledCommentString(comment, style), (int) width); boolean overflowing = false; if (height > OperatorAnnotation.MAX_HEIGHT) { height = OperatorAnnotation.MAX_HEIGHT; overflowing = true; } int opCenter = (int) (Double.parseDouble(x) + Double.parseDouble(w) / 2); int xCo = (int) (opCenter - width / 2); int yCo = (int) Double.parseDouble(y) + OperatorAnnotation.Y_OFFSET; OperatorAnnotation annotation = new OperatorAnnotation(comment, style, op, false, Boolean.parseBoolean(coloredStr), xCo, yCo, width, height); annotation.setOverflowing(overflowing); addOperatorAnnotation(annotation); } catch (NullPointerException | NumberFormatException e) { // operator annotations prior to 6.4.0 int width = OperatorAnnotation.DEFAULT_WIDTH; int height = AnnotationDrawUtils .getContentHeight(AnnotationDrawUtils.createStyledCommentString(comment, style), width); boolean overflowing = false; if (height > OperatorAnnotation.MAX_HEIGHT) { height = OperatorAnnotation.MAX_HEIGHT; overflowing = true; } int opCenter = (int) (Double.parseDouble(x) + Double.parseDouble(w) / 2); int xCo = opCenter - width / 2; int yCo = (int) Double.parseDouble(y) + OperatorAnnotation.Y_OFFSET; OperatorAnnotation annotation = new OperatorAnnotation(comment, new AnnotationStyle(AnnotationColor.TRANSPARENT, AnnotationAlignment.CENTER), op, false, false, xCo, yCo, width, height); annotation.setOverflowing(overflowing); addOperatorAnnotation(annotation); } } } } } } @Override public void executionUnitImported(final ExecutionUnit process, final Element element) { // workflow annotations NodeList children = element.getChildNodes(); children = element.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (child instanceof Element) { Element annotationElem = (Element) child; if (XML_TAG_ANNOTATION.equals(annotationElem.getTagName())) { Node textNode = annotationElem.getChildNodes().item(0); String comment = textNode != null ? textNode.getNodeValue() : ""; comment = comment == null ? "" : comment; String xStr = annotationElem.getAttribute(XML_ATTRIBUTE_X_POSITION); String yStr = annotationElem.getAttribute(XML_ATTRIBUTE_Y_POSITION); String wStr = annotationElem.getAttribute(XML_ATTRIBUTE_WIDTH); String hStr = annotationElem.getAttribute(XML_ATTRIBUTE_HEIGHT); String colorStr = annotationElem.getAttribute(XML_ATTRIBUTE_COLOR); String alignStr = annotationElem.getAttribute(XML_ATTRIBUTE_ALIGNMENT); String resizedStr = annotationElem.getAttribute(XML_ATTRIBUTE_RESIZED); String coloredStr = annotationElem.getAttribute(XML_ATTRIBUTE_COLORED); AnnotationStyle style = new AnnotationStyle(AnnotationColor.fromKey(colorStr), AnnotationAlignment.fromKey(alignStr)); try { double xLoc = Double.parseDouble(xStr); double yLoc = Double.parseDouble(yStr); double wLoc = Double.parseDouble(wStr); double hLoc = Double.parseDouble(hStr); boolean overflowing = false; int requiredHeight = AnnotationDrawUtils .getContentHeight(AnnotationDrawUtils.createStyledCommentString(comment, style), (int) wLoc); if (requiredHeight > hLoc) { overflowing = true; } ProcessAnnotation annotation = new ProcessAnnotation(comment, style, process, Boolean.parseBoolean(resizedStr), Boolean.parseBoolean(coloredStr), new Rectangle2D.Double(xLoc, yLoc, wLoc, hLoc)); annotation.setOverflowing(overflowing); addProcessAnnotation(annotation); } catch (NullPointerException | NumberFormatException e) { // ignore silently } } } } } /** * Returns the operator annotations for the given operator. * * @param operator * the operator in question * @return the annotations or {@code null} if there are none */ public static WorkflowAnnotations lookupOperatorAnnotations(Operator operator) { return (WorkflowAnnotations) operator.getUserData(KEY_OPERATOR_ANNOTATION); } /** * Adds a {@link OperatorAnnotation} to the {@link Operator}. * * @param annotation * the new annotation */ public static void addOperatorAnnotation(OperatorAnnotation annotation) { if (annotation == null) { throw new IllegalArgumentException("annotation must not be null!"); } WorkflowAnnotations annotations = lookupOperatorAnnotations(annotation.getAttachedTo()); if (annotations == null) { annotations = new WorkflowAnnotations(); } annotations.addAnnotation(annotation); annotation.getAttachedTo().setUserData(KEY_OPERATOR_ANNOTATION, annotations); } /** * Removes the given {@link OperatorAnnotation}. * * @param annotation * the annotation to remove */ public static void removeOperatorAnnotation(OperatorAnnotation annotation) { if (annotation == null) { throw new IllegalArgumentException("annotation must not be null!"); } WorkflowAnnotations annotations = lookupOperatorAnnotations(annotation.getAttachedTo()); if (annotations == null) { return; } annotations.removeAnnotation(annotation); annotation.getAttachedTo().setUserData(KEY_OPERATOR_ANNOTATION, annotations); } /** * Returns the process annotations for the given execution unit. * * @param process * the execution unit in question * @return the annotations or {@code null} if there are none */ public static WorkflowAnnotations lookupProcessAnnotations(ExecutionUnit process) { return (WorkflowAnnotations) process.getUserData(KEY_PROCESS_ANNOTATION); } /** * Adds a {@link ProcessAnnotation}. * * @param annotation * the new annotation */ public static void addProcessAnnotation(ProcessAnnotation annotation) { if (annotation == null) { throw new IllegalArgumentException("annotation must not be null!"); } WorkflowAnnotations annotations = lookupProcessAnnotations(annotation.getProcess()); if (annotations == null) { annotations = new WorkflowAnnotations(); } annotations.addAnnotation(annotation); annotation.getProcess().setUserData(KEY_PROCESS_ANNOTATION, annotations); } /** * Removes the given {@link ProcessAnnotation}. * * @param annotation * the annotation to remove */ public static void removeProcessAnnotation(ProcessAnnotation annotation) { if (annotation == null) { throw new IllegalArgumentException("annotation must not be null!"); } WorkflowAnnotations annotations = lookupProcessAnnotations(annotation.getProcess()); if (annotations == null) { return; } annotations.removeAnnotation(annotation); annotation.getProcess().setUserData(KEY_PROCESS_ANNOTATION, annotations); } }