/** * 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.gui.flow.processrendering.background; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import javax.imageio.ImageIO; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.ProgressThreadListener; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.UserData; import com.rapidminer.repository.BlobEntry; import com.rapidminer.repository.Entry; import com.rapidminer.repository.MalformedRepositoryLocationException; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; /** * Container for a process background image. * * @author Marco Boeck * @since 7.0.0 * */ public class ProcessBackgroundImage implements UserData<Object> { private int x; private int y; private int w; private int h; private String location; /** the process this background image is located in */ private ExecutionUnit process; /** the loaded image or an error image */ private volatile Image img; /** the image during loading */ private final Image loadingImg; private int loadingW; private int loadingH; private int errorW; private int errorH; /** whether the image has been loaded successfully */ private volatile boolean finishedImageLoading = false; /** whether the image failed to be loaded */ private volatile boolean errorImageLoading = false; /** whether loading of the image is in progress */ private AtomicBoolean loaded = new AtomicBoolean(false); /** * Creates a process background image at the given location. * * @param x * upper left x coordinate of the image. If set to {@code -1}, the image will be * centered horizontally * @param y * upper left y coordinate of the image. If set to {@code -1}, the image will be * centered vertically * @param w * width of the image. If set to {@code -1}, the width will be determined by the * image * @param h * height of the image. If set to {@code -1}, the height will be determined by the * image * @param location * repository location of the image * @param process * the process for which the image is */ public ProcessBackgroundImage(int x, int y, int w, int h, String location, ExecutionUnit process) { this.x = x; this.y = y; this.w = w; this.h = h; this.location = location; this.process = process; this.loadingImg = createImageFromString(I18N.getGUILabel("process_background.loading.label")); } /** * * @return x-coordinate of the background image. If {@code -1}, the image is centered */ public int getX() { return x; } /** * * @return y-coordinate of the background image. If {@code -1}, the image is centered */ public int getY() { return y; } /** * * @return width of the background image */ public int getWidth() { if (finishedImageLoading) { return w; } if (errorImageLoading) { return errorW; } return loadingW; } /** * * @return height of the background/loading/error image */ public int getHeight() { if (finishedImageLoading) { return h; } if (errorImageLoading) { return errorH; } return loadingH; } /** * * @return original width of the background image */ public int getOriginalWidth() { return w; } /** * * @return original height of the background image */ public int getOriginalHeight() { return h; } /** * * @return the repository location of the background image */ public String getLocation() { return location; } /** * Returns the background image. If it has not yet been loaded, will start asynchronous loading * of it and return a placeholder image until loading is complete. In case an error occurs * during loading, it will return an error image. * * @param listener * the listener for the {@link ProgressThread} loading the image. If no image needs * to be loaded, does nothing with it. Can be {@code null} * @return an image, never {@code null} */ public Image getImage(ProgressThreadListener listener) { if (finishedImageLoading) { return img; } if (errorImageLoading) { return img; } // only load once if (loaded.compareAndSet(false, true)) { ProgressThread pg = new ProgressThread("process_background.loading") { @Override public void run() { try { RepositoryLocation location = new RepositoryLocation(ProcessBackgroundImage.this.getLocation()); Entry entry = location.locateEntry(); if (entry == null) { LogService .getRoot() .log(Level.WARNING, "com.rapidminer.gui.flow.processrendering.background_image.ProcessBackgroundImageDecorator.missing"); img = createImageFromString(I18N.getGUILabel("process_background.loading.error.label")); errorImageLoading = true; return; } if (entry instanceof BlobEntry) { BlobEntry blob = (BlobEntry) entry; // try and create actual image img = createImageFromBlob(blob); if (img == null) { LogService .getRoot() .log(Level.WARNING, "com.rapidminer.gui.flow.processrendering.background_image.ProcessBackgroundImageDecorator.invalid_type"); img = createImageFromString(I18N.getGUILabel("process_background.loading.error.label")); errorImageLoading = true; return; } finishedImageLoading = true; if (w == -1 || h == -1) { w = img.getWidth(null); h = img.getHeight(null); } } else { LogService .getRoot() .log(Level.WARNING, "com.rapidminer.gui.flow.processrendering.background_image.ProcessBackgroundImageDecorator.invalid_type"); img = createImageFromString(I18N.getGUILabel("process_background.loading.error.label")); errorImageLoading = true; } } catch (RepositoryException | MalformedRepositoryLocationException e) { LogService .getRoot() .log(Level.WARNING, "com.rapidminer.gui.flow.processrendering.background_image.ProcessBackgroundImageDecorator.invalid_loc", ProcessBackgroundImage.this.getLocation()); img = createImageFromString(I18N.getGUILabel("process_background.loading.error.label")); errorImageLoading = true; } catch (IOException e) { LogService .getRoot() .log(Level.WARNING, "com.rapidminer.gui.flow.processrendering.background_image.ProcessBackgroundImageDecorator.invalid_type"); img = createImageFromString(I18N.getGUILabel("process_background.loading.error.label")); errorImageLoading = true; } } }; if (listener != null) { pg.addProgressThreadListener(listener); } pg.setIndeterminate(true); pg.start(); } return loadingImg; } /** * Returns the process this background image is in. * * @return the process */ public ExecutionUnit getProcess() { return process; } /** * {@inheritDoc} * * @param newParent * must be an {@link ExecutionUnit}. */ @Override public UserData<Object> copyUserData(Object newParent) { ProcessBackgroundImage copy = new ProcessBackgroundImage(x, y, w, h, location, process); return copy; } /** * Creates an {@link Image} which displays the given {@link String}. * * @param text * this text will be displayed in the image * @return the image, never {@code null} */ private Image createImageFromString(String text) { // to know bounds of desired text we need Graphics context so create fake one Graphics2D g2 = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).createGraphics(); Font font = new Font("Arial", Font.PLAIN, 24); g2.setFont(font); FontMetrics fm = g2.getFontMetrics(); // set intermediate width and height so we don't lose original height of background image // while loading and/or in error case loadingW = fm.stringWidth(text); loadingH = fm.getHeight(); errorW = loadingW; errorH = loadingH; g2.dispose(); // create actual image now that text bounds are known BufferedImage img = new BufferedImage(loadingW, loadingH, BufferedImage.TYPE_INT_ARGB); g2 = img.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.setFont(font); fm = g2.getFontMetrics(); g2.setColor(Colors.TEXT_FOREGROUND); g2.drawString(text, 0, fm.getAscent()); g2.dispose(); return img; } /** * Creates an {@link Image} from the given {@link BlobEntry}. * * @param entry * the blob entry which is expected to be an image * @return the image or {@code null} if it is not an image * @throws IOException * if the entry does not contain valid image data * @throws RepositoryException * if the entry could not be read */ private Image createImageFromBlob(BlobEntry entry) throws IOException, RepositoryException { InputStream is = entry.openInputStream(); BufferedImage img = ImageIO.read(is); is.close(); return img; } }