/*
* 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 com.facebook.presto.operator.scalar;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.function.LiteralParameters;
import com.facebook.presto.spi.function.ScalarFunction;
import com.facebook.presto.spi.function.SqlType;
import com.facebook.presto.spi.type.StandardTypes;
import com.facebook.presto.type.ColorType;
import com.facebook.presto.type.Constraint;
import com.google.common.annotations.VisibleForTesting;
import io.airlift.slice.Slice;
import java.awt.Color;
import static com.facebook.presto.operator.scalar.StringFunctions.upper;
import static com.facebook.presto.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT;
import static com.facebook.presto.util.Failures.checkCondition;
import static io.airlift.slice.Slices.utf8Slice;
import static java.lang.String.format;
public final class ColorFunctions
{
private static final String ANSI_RESET = "\u001b[0m";
private static final Slice RENDERED_TRUE = render(utf8Slice("\u2713"), color(utf8Slice("green")));
private static final Slice RENDERED_FALSE = render(utf8Slice("\u2717"), color(utf8Slice("red")));
public enum SystemColor
{
BLACK(0, "black"),
RED(1, "red"),
GREEN(2, "green"),
YELLOW(3, "yellow"),
BLUE(4, "blue"),
MAGENTA(5, "magenta"),
CYAN(6, "cyan"),
WHITE(7, "white");
private final int index;
private final String name;
SystemColor(int index, String name)
{
this.index = index;
this.name = name;
}
private int getIndex()
{
return index;
}
public String getName()
{
return name;
}
public static SystemColor valueOf(int index)
{
for (SystemColor color : values()) {
if (index == color.getIndex()) {
return color;
}
}
throw new PrestoException(GENERIC_INTERNAL_ERROR, "Invalid color index: " + index);
}
}
private ColorFunctions() {}
@ScalarFunction
@LiteralParameters("x")
@SqlType(ColorType.NAME)
public static long color(@SqlType("varchar(x)") Slice color)
{
int rgb = parseRgb(color);
if (rgb != -1) {
return rgb;
}
// encode system colors (0-15) as negative values, offset by one
try {
SystemColor systemColor = SystemColor.valueOf(upper(color).toStringUtf8());
int index = systemColor.getIndex();
return -(index + 1);
}
catch (IllegalArgumentException e) {
throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Invalid color: '%s'", color.toStringUtf8()), e);
}
}
@ScalarFunction
@SqlType(ColorType.NAME)
public static long rgb(@SqlType(StandardTypes.BIGINT) long red, @SqlType(StandardTypes.BIGINT) long green, @SqlType(StandardTypes.BIGINT) long blue)
{
checkCondition(red >= 0 && red <= 255, INVALID_FUNCTION_ARGUMENT, "red must be between 0 and 255");
checkCondition(green >= 0 && green <= 255, INVALID_FUNCTION_ARGUMENT, "green must be between 0 and 255");
checkCondition(blue >= 0 && blue <= 255, INVALID_FUNCTION_ARGUMENT, "blue must be between 0 and 255");
return (red << 16) | (green << 8) | blue;
}
/**
* Interpolate a color between lowColor and highColor based the provided value
* <p/>
* The value is truncated to the range [low, high] if it's outside.
* Color must be a valid rgb value of the form #rgb
*/
@ScalarFunction
@SqlType(ColorType.NAME)
public static long color(
@SqlType(StandardTypes.DOUBLE) double value,
@SqlType(StandardTypes.DOUBLE) double low,
@SqlType(StandardTypes.DOUBLE) double high,
@SqlType(ColorType.NAME) long lowColor,
@SqlType(ColorType.NAME) long highColor)
{
return color((value - low) * 1.0 / (high - low), lowColor, highColor);
}
/**
* Interpolate a color between lowColor and highColor based on the provided value
* <p/>
* The value is truncated to the range [0, 1] if necessary
* Color must be a valid rgb value of the form #rgb
*/
@ScalarFunction
@SqlType(ColorType.NAME)
public static long color(@SqlType(StandardTypes.DOUBLE) double fraction, @SqlType(ColorType.NAME) long lowColor, @SqlType(ColorType.NAME) long highColor)
{
checkCondition(lowColor >= 0, INVALID_FUNCTION_ARGUMENT, "lowColor not a valid RGB color");
checkCondition(highColor >= 0, INVALID_FUNCTION_ARGUMENT, "highColor not a valid RGB color");
fraction = Math.min(1, fraction);
fraction = Math.max(0, fraction);
return interpolate((float) fraction, lowColor, highColor);
}
@ScalarFunction
@LiteralParameters({"x", "y"})
@Constraint(variable = "y", expression = "min(2147483647, x + 15)")
// Color formatting uses 15 characters. Note that if the ansiColorEscape function implementation
// changes, this value may be invalidated.
@SqlType("varchar(y)")
public static Slice render(@SqlType("varchar(x)") Slice value, @SqlType(ColorType.NAME) long color)
{
StringBuilder builder = new StringBuilder(value.length());
// color
builder.append(ansiColorEscape(color))
.append(value.toStringUtf8())
.append(ANSI_RESET);
return utf8Slice(builder.toString());
}
@ScalarFunction
@SqlType("varchar(35)")
public static Slice render(@SqlType(StandardTypes.BIGINT) long value, @SqlType(ColorType.NAME) long color)
{
return render(utf8Slice(Long.toString(value)), color);
}
@ScalarFunction
@SqlType("varchar(41)")
public static Slice render(@SqlType(StandardTypes.DOUBLE) double value, @SqlType(ColorType.NAME) long color)
{
return render(utf8Slice(Double.toString(value)), color);
}
@ScalarFunction
@SqlType("varchar(16)")
public static Slice render(@SqlType(StandardTypes.BOOLEAN) boolean value)
{
return value ? RENDERED_TRUE : RENDERED_FALSE;
}
@ScalarFunction
@SqlType(StandardTypes.VARCHAR)
public static Slice bar(@SqlType(StandardTypes.DOUBLE) double percent, @SqlType(StandardTypes.BIGINT) long width)
{
return bar(percent, width, rgb(255, 0, 0), rgb(0, 255, 0));
}
@ScalarFunction
@SqlType(StandardTypes.VARCHAR)
public static Slice bar(
@SqlType(StandardTypes.DOUBLE) double percent,
@SqlType(StandardTypes.BIGINT) long width,
@SqlType(ColorType.NAME) long lowColor,
@SqlType(ColorType.NAME) long highColor)
{
long count = (int) (percent * width);
count = Math.min(width, count);
count = Math.max(0, count);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < count; i++) {
float fraction = (float) (i * 1.0 / (width - 1));
int color = interpolate(fraction, lowColor, highColor);
builder.append(ansiColorEscape(color))
.append('\u2588');
}
// reset
builder.append(ANSI_RESET);
// pad to force column to be the requested width
for (long i = count; i < width; ++i) {
builder.append(' ');
}
return utf8Slice(builder.toString());
}
private static int interpolate(float fraction, long lowRgb, long highRgb)
{
float[] lowHsv = Color.RGBtoHSB(getRed(lowRgb), getGreen(lowRgb), getBlue(lowRgb), null);
float[] highHsv = Color.RGBtoHSB(getRed(highRgb), getGreen(highRgb), getBlue(highRgb), null);
float h = fraction * (highHsv[0] - lowHsv[0]) + lowHsv[0];
float s = fraction * (highHsv[1] - lowHsv[1]) + lowHsv[1];
float v = fraction * (highHsv[2] - lowHsv[2]) + lowHsv[2];
return Color.HSBtoRGB(h, s, v) & 0xFF_FF_FF;
}
/**
* Convert the given color (rgb or system) to an ansi-compatible index (for use with ESC[38;5;<value>m)
*/
private static int toAnsi(int red, int green, int blue)
{
// rescale to 0-5 range
red = red * 6 / 256;
green = green * 6 / 256;
blue = blue * 6 / 256;
return 16 + red * 36 + green * 6 + blue;
}
private static String ansiColorEscape(long color)
{
return "\u001b[38;5;" + toAnsi(color) + 'm';
}
/**
* Convert the given color (rgb or system) to an ansi-compatible index (for use with ESC[38;5;<value>m)
*/
private static int toAnsi(long color)
{
if (color >= 0) { // an rgb value encoded as in Color.getRGB
return toAnsi(getRed(color), getGreen(color), getBlue(color));
}
else {
return (int) (-color - 1);
}
}
@VisibleForTesting
static int parseRgb(Slice color)
{
if (color.length() != 4 || color.getByte(0) != '#') {
return -1;
}
int red = Character.digit((char) color.getByte(1), 16);
int green = Character.digit((char) color.getByte(2), 16);
int blue = Character.digit((char) color.getByte(3), 16);
if (red == -1 || green == -1 || blue == -1) {
return -1;
}
// replicate the nibbles to turn a color of the form #rgb => #rrggbb (css semantics)
red = (red << 4) | red;
green = (green << 4) | green;
blue = (blue << 4) | blue;
return (int) rgb(red, green, blue);
}
@VisibleForTesting
static int getRed(long color)
{
checkCondition(color >= 0, INVALID_FUNCTION_ARGUMENT, "color is not a valid rgb value");
return (int) ((color >>> 16) & 0xff);
}
@VisibleForTesting
static int getGreen(long color)
{
checkCondition(color >= 0, INVALID_FUNCTION_ARGUMENT, "color is not a valid rgb value");
return (int) ((color >>> 8) & 0xff);
}
@VisibleForTesting
static int getBlue(long color)
{
checkCondition(color >= 0, INVALID_FUNCTION_ARGUMENT, "color is not a valid rgb value");
return (int) (color & 0xff);
}
}