content: // ===============================================================================
  // Authors: AFRL/RQQD
  // Organization: Air Force Research Laboratory, Aerospace Systems Directorate, Power and Control Division
  // 
  // Copyright (c) 2017 Government of the United State of America, as represented by
  // the Secretary of the Air Force.  No copyright is claimed in the United States under
  // Title 17, U.S. Code.  All Other Rights Reserved.
  // ===============================================================================
  
  /*
   * GRAL: GRAphing Library for Java(R)
   *
   * (C) Copyright 2009-2013 Erich Seifert <dev[at]erichseifert.de>,
   * Michael Seifert <mseifert[at]error-reports.org>
   *
   * This file is part of GRAL.
   *
   * GRAL is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * GRAL 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 GRAL.  If not, see <http://www.gnu.org/licenses/>.
   */
  package de.erichseifert.gral.util;
  
  import java.awt.Color;
  import java.awt.Font;
  import java.awt.Graphics2D;
  import java.awt.Paint;
  import java.awt.Shape;
  import java.awt.Stroke;
  import java.awt.font.FontRenderContext;
  import java.awt.font.LineBreakMeasurer;
  import java.awt.font.TextAttribute;
  import java.awt.font.TextLayout;
  import java.awt.geom.AffineTransform;
  import java.awt.geom.Area;
  import java.awt.geom.Rectangle2D;
  import java.text.AttributedCharacterIterator;
  import java.text.AttributedString;
  import java.util.LinkedList;
  import java.util.List;
  
  /**
   * Abstract class that contains utility functions for working with graphics.
   * For example, this includes font handling or color space conversion.
   */
  public abstract class GraphicsUtils {
  	/** Default font render context. */
  	private static final FontRenderContext frc = new FontRenderContext(null, true, true);
  
  	/** Constant for the CIE XYZ and CIE L*u*v* color spaces: (6/29)^3 **/
  	private static final double CIE_EPSILON = 216.0/24389.0;
  	/** Constant for the CIE XYZ and CIE L*u*v* color spaces: (29/3)^3 **/
  	private static final double CIE_KAPPA = 24389.0/27.0;
  
  	/** Xr, Yr, Zr constants with D50 white point used for CIE XYZ to
  	CIE L*u*v* conversion **/
  	private static final double[] XYZ_R_D50 = {
  		0.964221, 1.000000, 0.825211
  	};
  	/** Precalculated u0 constant for CIE L*u*v* to CIE XYZ conversion. **/
  	private static final double XYZ_R_D50_U0 =
  		4.0*XYZ_R_D50[0]/(XYZ_R_D50[0] + 15.0*XYZ_R_D50[1] + 3.0*XYZ_R_D50[2]);
  	/** Precalculated v0 constant for CIE L*u*v* to CIE XYZ conversion. **/
  	private static final double XYZ_R_D50_V0 =
  		9.0*XYZ_R_D50[1]/(XYZ_R_D50[0] + 15.0*XYZ_R_D50[1] + 3.0*XYZ_R_D50[2]);
  
  	/** sRGB to CIE XYZ conversion matrix. See
  	http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html#Specifications **/
  	private static final double[] MATRIX_SRGB2XYZ_D50 = {
  		0.436052025, 0.385081593, 0.143087414,
  		0.222491598, 0.716886060, 0.060621486,
  		0.013929122, 0.097097002, 0.714185470
  	};
  
  	/** CIE XYZ to sRGB conversion matrix. See
  	http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html#Specifications **/
  	private static final double[] MATRIX_XYZ2SRGB_D50 = {
  		 3.1338561, -1.6168667, -0.4906146,
  		-0.9787684,  1.9161415,  0.0334540,
  		 0.0719453, -0.2289914,  1.4052427
  	};
  
  	/**
  	 * Default constructor that prevents creation of class.
  	 */
  	protected GraphicsUtils() {
  		throw new UnsupportedOperationException();
  	}
  
  	/**
  	 * Returns the outline for the specified text using the specified font and
  	 * line width. The text may also contain line breaks ({@literal '\n'}).
  	 * @param text Text to be displayed.
  	 * @param font Font of the Text.
  	 * @param wrappingWidth Maximum width of lines
  	 * @param alignment Alignment of the text when it spans multiple lines.
  	 * @return Shape of the text outline in the specified font.
  	 */
  	public static Shape getOutline(String text, Font font, float wrappingWidth,
  			double alignment) {
  		boolean wordWrap = true;
  		if (wrappingWidth <= 0f) {
  			wordWrap = false;
  			wrappingWidth = Float.MAX_VALUE;
  		}
  
  		AttributedString string = new AttributedString(text);
  		string.addAttribute(TextAttribute.FONT, font);
  		AttributedCharacterIterator iterator = string.getIterator();
  		LineBreakMeasurer measurer = new LineBreakMeasurer(iterator, frc);
  
  		List<TextLayout> lines = new LinkedList<TextLayout>();
  		while (measurer.getPosition() < text.length()) {
  			// Find out which character will be wrapped next
  			int nextBreakPos = measurer.nextOffset(wrappingWidth);
  			int lineBreakPos = text.indexOf('\n', measurer.getPosition()) + 1;
  
  			int breakPos = nextBreakPos;
  			if (lineBreakPos > 0 && lineBreakPos < nextBreakPos) {
  				breakPos = lineBreakPos;
  			}
  			TextLayout line = measurer.nextLayout(wrappingWidth, breakPos, false);
  			lines.add(line);
  		}
  
  		if (!wordWrap) {
  			// Determine the maximal line length
  			float advanceMax = 0f;
  			for (TextLayout line : lines) {
  				advanceMax = Math.max(line.getAdvance(), advanceMax);
  			}
  			wrappingWidth = advanceMax;
  		}
  
  		AffineTransform txLinePos = new AffineTransform();
  		Area outlineAllLines = null;
  		for (TextLayout line : lines) {
  			// Distribute the space that's left
  			double dx = alignment*(wrappingWidth - line.getAdvance());
  
  			// Move to baseline
  			txLinePos.translate(dx, line.getAscent());
  			// Get the shape of the current line
  			Area outlineLine = new Area(line.getOutline(txLinePos));
  			// Add the shape of the line to the shape
  			if (outlineAllLines == null) {
  				outlineAllLines = outlineLine;
  			} else {
  				outlineAllLines.add(outlineLine);
  			}
  
  			// Move to next line
  			txLinePos.translate(-dx, line.getDescent() + line.getLeading());
  		}
  
  		return outlineAllLines;
  	}
  
  	/**
  	 * Fills a Shape with the specified Paint object.
  	 * @param graphics Graphics to be painted into.
  	 * @param shape Shape to be filled.
  	 * @param paint Paint to be used.
  	 * @param paintBounds Optional bounds describing the painted area.
  	 */
  	public static void fillPaintedShape(Graphics2D graphics, Shape shape,
  			Paint paint, Rectangle2D paintBounds) {
  		if (shape == null) {
  			return;
  		}
  		if (paintBounds == null) {
  			paintBounds = shape.getBounds2D();
  		}
  		AffineTransform txOrig = graphics.getTransform();
  		graphics.translate(paintBounds.getX(), paintBounds.getY());
  		graphics.scale(paintBounds.getWidth(), paintBounds.getHeight());
  		Paint paintOld = null;
  		if (paint != null) {
  			paintOld = graphics.getPaint();
  			graphics.setPaint(paint);
  		}
  		AffineTransform tx = AffineTransform.getScaleInstance(
  				1.0/paintBounds.getWidth(), 1.0/paintBounds.getHeight());
  		tx.translate(-paintBounds.getX(), -paintBounds.getY());
  		graphics.fill(tx.createTransformedShape(shape));
  		if (paintOld != null) {
  			graphics.setPaint(paintOld);
  		}
  		graphics.setTransform(txOrig);
  	}
  
  	/**
  	 * Draws a filled Shape with the specified Paint object.
  	 * @param graphics Graphics to be painted into.
  	 * @param shape Shape to be filled.
  	 * @param paint Paint to be used.
  	 * @param paintBounds Optional bounds describing the painted area.
  	 * @param stroke Stroke to be used for outlines.
  	 */
  	public static void drawPaintedShape(Graphics2D graphics, Shape shape,
  			Paint paint, Rectangle2D paintBounds, Stroke stroke) {
  		if (shape == null) {
  			return;
  		}
  		if (stroke == null) {
  			stroke = graphics.getStroke();
  		}
  		shape = stroke.createStrokedShape(shape);
  		fillPaintedShape(graphics, shape, paint, paintBounds);
  	}
  
  	/**
  	 * Converts color components from the sRGB to the CIE XYZ color space.
  	 * A D50 white point is assumed for the sRGB conversion. If the <i>xyz</i>
  	 * array is {@code null}, a new one will be created with the same
  	 * size as the <i>rgb</i> array.
  	 *
  	 * See http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
  	 *
  	 * @param rgb Color components in the sRGB color space.
  	 * @param xyz Optional array to store color components in the CIE XYZ color space.
  	 * @return Color components in the CIE XYZ color space.
  	 */
  	public static double[] rgb2xyz(double[] rgb, double[] xyz) {
  		if (xyz == null) {
  			xyz = new double[rgb.length];
  		}
  
  		// Remove sRGB companding to make RGB components linear
  		double[] rgbLin = new double[rgb.length];
  		for (int i = 0; i < rgb.length; i++) {
  			if (rgb[i] <= 0.04045) {
  				rgbLin[i] = rgb[i]/12.92;
  			} else {
  				rgbLin[i] = Math.pow((rgb[i] + 0.055)/1.055, 2.4);
  			}
  		}
  
  		// Convert linear sRGB with D50 white point to CIE XYZ
  		for (int i = 0; i < xyz.length; i++) {
  			xyz[i] = MATRIX_SRGB2XYZ_D50[i*3 + 0]*rgbLin[0] +
                       MATRIX_SRGB2XYZ_D50[i*3 + 1]*rgbLin[1] +
                       MATRIX_SRGB2XYZ_D50[i*3 + 2]*rgbLin[2];
  		}
  
  		return xyz;
  	}
  
  	/**
  	 * Convert color components from the CIE L*u*v* to the CIE XYZ color space.
  	 * If the <i>xyz</i> array is {@code null}, a new one will be created
  	 * with the same size as the <i>luv</i> array.
  	 *
  	 * See http://www.brucelindbloom.com/index.html?Eqn_Luv_to_XYZ.html
  	 *
  	 * @param luv Color components in the CIE L*u*v* color space
  	 * @param xyz Optional array to store color components in the CIE XYZ color
  	 *            space.
  	 * @return Color components in the CIE XYZ color space.
  	 */
  	public static double[] luv2xyz(double[] luv, double[] xyz) {
  		if (xyz == null) {
  			xyz = new double[luv.length];
  		}
  
  		if (luv[0] > CIE_KAPPA*CIE_EPSILON) {
  			xyz[1] = (luv[0] + 16.0)/116.0;
  			xyz[1] = xyz[1]*xyz[1]*xyz[1];
  		} else {
  			xyz[1] = luv[0]/CIE_KAPPA;
  		}
  
  		double a = (luv[0] != 0.0 || luv[1] != 0.0)
  			? ((52.0*luv[0])/(luv[1] + 13.0*luv[0]*XYZ_R_D50_U0) - 1.0)/3.0
  			: 0.0;
  		double b = -5*xyz[1];
  		double c = -1.0/3.0;
  		double d = (luv[0] != 0.0 || luv[2] != 0.0)
  			? xyz[1]*((39.0*luv[0])/(luv[2] + 13.0*luv[0]*XYZ_R_D50_V0) - 5.0)
  			: 0.0;
  
  		xyz[0] = !MathUtils.almostEqual(a, c, 1e-15) ? (d - b)/(a - c) : 0.0;
  		xyz[2] = xyz[0]*a + b;
  
  		return xyz;
  	}
  
  	/**
  	 * Converts color components from the sRGB to the CIE XYZ color space.
  	 * A D50 white point is assumed for the sRGB conversion. If the <i>rgb</i>
  	 * array is {@code null}, a new one will be created with the same
  	 * size as the <i>xyz</i> array.
  	 *
  	 * See http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
  	 *
  	 * @param xyz Color components in the CIE XYZ color space.
  	 * @param rgb Optional array for storing color components in the sRGB color
  	 *            space.
  	 * @return Color components in the sRGB color space.
  	 */
  	public static double[] xyz2rgb(double[] xyz, double[] rgb) {
  		if (rgb == null) {
  			rgb = new double[xyz.length];
  		}
  
  		// XYZ to linear sRGB with D50 white point
  		for (int i = 0; i < xyz.length; i++) {
  			rgb[i] = MATRIX_XYZ2SRGB_D50[i*3 + 0]*xyz[0] +
  					 MATRIX_XYZ2SRGB_D50[i*3 + 1]*xyz[1] +
  					 MATRIX_XYZ2SRGB_D50[i*3 + 2]*xyz[2];
  		}
  
  		// Apply sRGB companding
  		for (int i = 0; i < rgb.length; i++) {
  			if (rgb[i] <= 0.0031308) {
  				rgb[i] = 12.92*rgb[i];
  			} else {
  				rgb[i] = 1.055*Math.pow(rgb[i], 1.0/2.4) - 0.055;
  			}
  		}
  
  		return rgb;
  	}
  
  	/**
  	 * Converts color components from the CIE XYZ to the CIE L*u*v* color
  	 * space. If the <i>luv</i> array is {@code null}, a new one will be
  	 * created with the same size as the <i>xyz</i> array.
  	 *
  	 * http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Luv.html
  	 *
  	 * @param xyz Color components in the CIE XYZ color space.
  	 * @param luv Optional array for storing color components in the CIE L*u*v*
  	 *            color space.
  	 * @return Color components in the CIE L*u*v* color space.
  	 */
  	public static double[] xyz2luv(double[] xyz, double[] luv) {
  		double tmp = xyz[0] + 15.0*xyz[1] + 3.0*xyz[2];
  		if (tmp == 0.0) {
  			tmp = 1.0;
  		}
  		double u1 = 4.0*xyz[0]/tmp;
  		double v1 = 9.0*xyz[1]/tmp;
  
  		// Relative luminance
  		double yr = xyz[1]/XYZ_R_D50[1];
  		double ur = 4.0*XYZ_R_D50[0]/(XYZ_R_D50[0] + 15.0*XYZ_R_D50[1] + 3.0*XYZ_R_D50[2]);
  		double vr = 9.0*XYZ_R_D50[1]/(XYZ_R_D50[0] + 15.0*XYZ_R_D50[1] + 3.0*XYZ_R_D50[2]);
  
  		// Mapping relative luminance to lightness
  		if (luv == null) {
  			luv = new double[xyz.length];
  		}
  		if (yr > CIE_EPSILON) {
  			luv[0] = 116.0*Math.pow(yr, 1.0/3.0) - 16.0;
  		} else {
  			luv[0] = CIE_KAPPA*yr;
  		}
  		luv[1] = 13.0*luv[0]*(u1 - ur);
  		luv[2] = 13.0*luv[0]*(v1 - vr);
  
  		return luv;
  	}
  
  	/**
  	 * Converts color components from the CIE L*u*v* to the sRGB color space.
  	 * A D50 white point is assumed for the sRGB conversion. If the <i>luv</i>
  	 * array is {@code null}, a new one will be created with the same
  	 * size as the <i>rgb</i> array.
  	 *
  	 * @param rgb Color components in the sRGB color space.
  	 * @param luv Optional array for storing color components in the CIE L*u*v*
  	 *            color space.
  	 * @return Color components in the CIE L*u*v* color space.
  	 */
  	public static double[] rgb2luv(double[] rgb, double[] luv) {
  		double[] xyz = rgb2xyz(rgb, null);
  		return xyz2luv(xyz, luv);
  	}
  
  	/**
  	 * Converts color components from the CIE L*u*v* to the sRGB color space.
  	 * A D50 white point is assumed for the sRGB conversion. If the <i>rgb</i>
  	 * array is {@code null}, a new one will be created with the same size
  	 * as the <i>luv</i> array.
  	 *
  	 * @param luv Color components in the CIE L*u*v* color space.
  	 * @param rgb Optional array for storing color components in the sRGB color
  	 *            space.
  	 * @return Color components in sRGB color space.
  	 */
  	public static double[] luv2rgb(double[] luv, double[] rgb) {
  		double[] xyz = luv2xyz(luv, null);
  		return xyz2rgb(xyz, rgb);
  	}
  
  	/**
  	 * Linearly blends two colors with a defined weight.
  	 * @param color1 First color.
  	 * @param color2 Second color.
  	 * @param weight Weighting factor in the range 0 to 1 (0 means color1, 1 means second color)
  	 * @return New blended color
  	 */
  	public static Color blend(Color color1, Color color2, double weight) {
  		double w2 = MathUtils.limit(weight, 0.0, 1.0);
  		double w1 = 1.0 - w2;
  		int r = (int) Math.round(w1*color1.getRed()   + w2*color2.getRed());
  		int g = (int) Math.round(w1*color1.getGreen() + w2*color2.getGreen());
  		int b = (int) Math.round(w1*color1.getBlue()  + w2*color2.getBlue());
  		int a = (int) Math.round(w1*color1.getAlpha() + w2*color2.getAlpha());
  		return new Color(r, g, b, a);
  	}
  
  	/**
  	 * Creates a new color with the same color components but a different
  	 * alpha value.
  	 * @param color Original color.
  	 * @param alpha Alpha value for new color.
  	 * @return New color with specified alpha value.
  	 */
  	public static Color deriveWithAlpha(Color color, int alpha) {
  		return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
  	}
  
  	/**
  	 * Creates a new darker version of a color by blending it with black. The
  	 * derived color has the same alpha value as the original color.
  	 * @param color Original color.
  	 * @return Darker color with same alpha value.
  	 */
  	public static Color deriveDarker(Color color) {
  		return deriveWithAlpha(blend(color, Color.BLACK, 0.5), color.getAlpha());
  	}
  
  	/**
  	 * Creates a new brighter version of a color by blending it with white. The
  	 * derived color has the same alpha value as the original color.
  	 * @param color Original color.
  	 * @return Brighter color with same alpha value.
  	 */
  	public static Color deriveBrighter(Color color) {
  		return deriveWithAlpha(blend(color, Color.WHITE, 0.5), color.getAlpha());
  	}
  
  }
  