org.mrgeo.colorscale.ColorScale.java Source code

Java tutorial

Introduction

Here is the source code for org.mrgeo.colorscale.ColorScale.java

Source

/*
 * Copyright 2009-2016 DigitalGlobe, Inc.
 *
 * 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.mrgeo.colorscale;

import org.apache.commons.io.IOUtils;
import org.codehaus.jackson.map.ObjectMapper;
import org.mrgeo.utils.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import java.awt.*;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

/**
 * @author jason.surratt
 *         <p/>
 *         Example file format: <ColorMap name="My Color Map"> <Scaling>MinMax</Scaling> <!-defaults
 *         to Absolute --> <ReliefShading>0</ReliefShading> <!-- defaults to 0/false -->
 *         <Interpolate>1</Interpolate> <!-- defaults to 1/true --> <NullColor color="0,0,0"
 *         opacity="0"> <!-- defaults to transparent --> <Color value="0.0" color="255,0,0"
 *         opacity="255"> <Color value="0.2" color="255,255,0"> <!-- if not specified an opacity
 *         defaults to 255 --> <Color value="0.4" color="0,255,0"> <Color value="0.6"
 *         color="0,255,255"> <Color value="0.8" color="0,0,255"> <Color value="1.0"
 *         color="255,0,255"> </ColorMap>
 */
public class ColorScale extends TreeMap<Double, Color> {

    private static final long serialVersionUID = 1L;
    private static final int CACHE_SIZE = 1024;
    private static final int A = 3;
    private static final int B = 2;
    private static final int G = 1;
    private static final int R = 0;
    private static ColorScale _colorScale = null;
    private static ColorScale _grayScale = null;
    private int[][] cache = null;
    private boolean interpolate;
    private Double min, max;
    private int[] nullColor = { 0, 0, 0, 0 };
    private boolean reliefShading;
    private Scaling scaling = Scaling.Absolute;
    private boolean forceValuesIntoRange;
    private double transparent = Double.NaN;
    private String name = null;
    private String title = null;
    private String description = null;

    private ColorScale() {
        clear();
    }

    public static ColorScale loadFromXML(InputStream stream) throws ColorScaleException {
        ColorScale cs = new ColorScale();
        cs.fromXML(stream);

        return cs;
    }

    public static ColorScale loadFromXML(String filename) throws ColorScaleException {
        try {
            InputStream stream = null;
            try {
                stream = new FileInputStream(filename);
                ColorScale cs = new ColorScale();
                cs.fromXML(stream);

                return cs;
            } finally {
                if (stream != null) {
                    IOUtils.closeQuietly(stream);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new ColorScaleException(e);
        }
    }

    public static ColorScale loadFromJSON(InputStream stream) throws ColorScaleException {
        ColorScale cs = new ColorScale();
        cs.fromJSON(stream);
        return cs;
    }

    public static ColorScale loadFromJSON(String json) throws ColorScaleException {
        ColorScale cs = new ColorScale();
        cs.fromJSON(json);
        return cs;
    }

    public static ColorScale empty() {
        return new ColorScale();
    }

    /**
     * Method returns a default color scale.
     *
     * @return ColorScale An object representing the color scale.
     */
    public static ColorScale createDefault() {
        // Make sure the color scale has been initiated before returning
        if (_colorScale == null || _colorScale.isEmpty()) {
            _colorScale = new ColorScale();
            _colorScale.setDefaultValues();
        }
        return (ColorScale) _colorScale.clone();
    }

    public static ColorScale createDefaultGrayScale() {
        // Make sure the color scale has been initiated before returning
        if (_grayScale == null || _grayScale.isEmpty()) {
            _grayScale = new ColorScale();
            _grayScale.setDefaultGrayScaleValues();
        }
        return (ColorScale) _grayScale.clone();
    }

    /**
     * Method to set the default color scale. The source for the color scale can come from either an
     * xml file or from hard coded values in the absent of the file.
     *
     * @param colorScale The URI of the color xml file.
     */
    public static void setDefault(final ColorScale colorScale) {
        _colorScale = colorScale;

        if (_colorScale == null) {
            _colorScale = new ColorScale();
            _colorScale.setDefaultValues();
        }
    }

    private static void parseColor(final String colorStr, final String opacityStr, final int[] color)
            throws IOException {
        final String[] colors = colorStr.split(",");
        if (colors.length != 3) {
            throw new IOException("Error parsing XML: There must be three elements in color.");
        }
        color[0] = Integer.valueOf(colors[0]);
        color[1] = Integer.valueOf(colors[1]);
        color[2] = Integer.valueOf(colors[2]);
        if (opacityStr != null && !opacityStr.isEmpty()) {
            color[3] = Integer.valueOf(opacityStr);
        } else {
            color[3] = 255;
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getTitle() {
        return title;
    }

    //  public static ColorScale loadFromJSONFile(String filename) throws ColorScaleException
    //  {
    //    try
    //    {
    //      InputStream stream;
    //      try
    //      {
    //       stream = HadoopFileUtils.open(new Path(filename));
    //      }
    //      catch (FileNotFoundException e)
    //      {
    //        // if the file wasn't found, try a local file
    //        LocalFileSystem lf = FileSystem.getLocal(HadoopUtils.createConfiguration());
    //        stream = lf.open(new Path(filename));
    //      }
    //      ColorScale cs = new ColorScale();
    //      cs.fromJSON(stream);
    //      IOUtils.closeQuietly(stream);
    //
    //      return cs;
    //    }
    //    catch (IOException e)
    //    {
    //      e.printStackTrace();
    //      throw new ColorScaleException(e);
    //    }
    //  }

    public String getDescription() {
        return description;
    }

    @Override
    public void clear() {
        super.clear();
        scaling = Scaling.Absolute;
        reliefShading = false;
        interpolate = true;
        forceValuesIntoRange = false;
        min = null;
        max = null;
        cache = null;
    }
    //
    //  public ColorScale(final InputStream strm) throws ColorScaleException
    //  {
    //    load(strm);
    //  }
    //
    //  public ColorScale(final String filename) throws ColorScaleException
    //  {
    //    load(filename);
    //  }

    @Override
    public Object clone() {
        super.clone();

        final ColorScale result = new ColorScale();
        result.cache = null;
        result.interpolate = interpolate;
        result.min = min;
        result.max = max;
        result.nullColor = nullColor.clone();
        result.reliefShading = reliefShading;
        result.scaling = scaling;
        result.transparent = transparent;
        result.forceValuesIntoRange = forceValuesIntoRange;
        result.putAll(this);

        return result;
    }

    public boolean equals(final ColorScale cs) {
        if (min == null && cs.min != null || min != null && cs.min == null) {
            return false;
        }
        if ((min != null && cs.min != null) && Double.compare(min, cs.min) != 0) {
            return false;
        }
        if (max == null && cs.max != null || max != null && cs.max == null) {
            return false;
        }
        if ((max != null && cs.max != null) && Double.compare(max, cs.max) != 0) {
            return false;
        }
        if (!scaling.equals(cs.scaling)) {
            return false;
        }
        if (interpolate != cs.interpolate) {
            return false;
        }
        if (forceValuesIntoRange != cs.forceValuesIntoRange) {
            return false;
        }
        if (reliefShading != cs.reliefShading) {
            return false;
        }
        if (nullColor.length != cs.nullColor.length) {
            return false;
        }
        for (int i = 0; i < nullColor.length; i++) {
            if (nullColor[i] != cs.nullColor[i]) {
                return false;
            }
        }
        if (size() != cs.size()) {
            return false;
        }
        final Iterator<Double> iterator1 = cs.keySet().iterator();
        for (final Double d1 : this.keySet()) {
            final Double d2 = iterator1.next();
            if (d1.compareTo(d2) != 0) {
                return false;
            }

            final Color value1 = get(d1);
            final Color value2 = get(d2);
            if (!value1.equals(value2)) {
                return false;
            }
        }
        return true;
    }

    public boolean getForceValuesIntoRange() {
        return forceValuesIntoRange;
    }

    /**
     * If true, values below the min will be assigned the min color while those above the max will be
     * assigned the max color. If false, values outside the min/max range will be assigned the nodata
     * color.
     *
     * @param i If true interpolation is enabled.
     */
    public void setForceValuesIntoRange(final boolean i) {
        forceValuesIntoRange = i;
    }

    public boolean getInterpolate() {
        return interpolate;
    }

    /**
     * If true, the colors in the color scale will be interpolated. For instance if 0 is mapped to
     * black and 1 is mapped to white then .5 would be a grey. If interpolate is set to false then the
     * nearest color that is <= to the value will be used. E.g. .5 would be black.
     *
     * @param i If true interpolation is enabled.
     */
    public void setInterpolate(final boolean i) {
        interpolate = i;
        cache = null;
    }

    public Double getMax() {
        return max;
    }

    public Double getMin() {
        return min;
    }

    public int[] getNullColor() {
        return nullColor;
    }

    public void setNullColor(final int[] color) {
        nullColor = color;
    }

    final public boolean getReliefShading() {
        return reliefShading;
    }

    public Scaling getScaling() {
        return scaling;
    }

    public void setScaling(final Scaling s) {
        scaling = s;
        cache = null;
    }

    public double getTransparent() {
        return transparent;
    }

    public void setTransparent(final double transparent) {
        this.transparent = transparent;
    }

    public void fromXML(final Document doc) throws ColorScaleException {
        try {
            clear();

            final XPath xpath = XmlUtils.createXPath();

            name = xpath.evaluate("/ColorMap/@name", doc);

            final Node nodeTitle = (Node) xpath.evaluate("/ColorMap/Title", doc, XPathConstants.NODE);
            if (nodeTitle != null) {
                title = xpath.evaluate("text()", nodeTitle);
            }

            final Node nodeDesc = (Node) xpath.evaluate("/ColorMap/Description", doc, XPathConstants.NODE);
            if (nodeDesc != null) {
                description = xpath.evaluate("text()", nodeDesc);
            }

            scaling = Scaling.valueOf(xpath.evaluate("/ColorMap/Scaling/text()", doc));
            final String reliefShadingStr = xpath.evaluate("/ColorMap/ReliefShading/text()", doc).toLowerCase();
            reliefShading = reliefShadingStr.equals("1") || reliefShadingStr.equals("true");

            final String interpolateStr = xpath.evaluate("/ColorMap/Interpolate/text()", doc).toLowerCase();
            interpolate = interpolateStr.isEmpty() || (interpolateStr.equals("1") || interpolateStr.equals("true"));

            final String forceStr = xpath.evaluate("/ColorMap/ForceValuesIntoRange/text()", doc).toLowerCase();
            forceValuesIntoRange = (forceStr.equals("1") || forceStr.equals("true"));

            final Node nullColorNode = (Node) xpath.evaluate("/ColorMap/NullColor", doc, XPathConstants.NODE);
            if (nullColorNode != null) {
                final String colorStr = xpath.evaluate("@color", nullColorNode);
                final String opacityStr = xpath.evaluate("@opacity", nullColorNode);
                parseColor(colorStr, opacityStr, nullColor);
            }

            final int[] color = new int[4];
            final XPathExpression expr = xpath.compile("/ColorMap/Color");
            final NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
            for (int i = 0; i < nodes.getLength(); i++) {
                final Node node = nodes.item(i);

                final String valueStr = xpath.evaluate("@value", node);
                final String colorStr = xpath.evaluate("@color", node);
                final String opacityStr = xpath.evaluate("@opacity", node);

                if (valueStr.isEmpty()) {
                    throw new IOException("Error parsing XML: A value must be specified for a color element.");
                }

                final double value = Double.valueOf(valueStr);
                parseColor(colorStr, opacityStr, color);
                put(value, color);
            }
            cache = null;
        } catch (final Exception e) {
            throw new BadXMLException(e);
        }
    }

    public void fromXML(final InputStream strm) throws ColorScaleException {
        try {
            fromXML(XmlUtils.parseInputStream(strm));
        } catch (final Exception e) {
            throw new ColorScaleException(e);
        }
    }

    //  public void load(final String filename) throws ColorScaleException
    //  {
    //    try
    //    {
    //      load(new FileInputStream(new File(filename)));
    //    }
    //    catch (final Exception e)
    //    {
    //      throw new BadFileException(e);
    //    }
    //  }

    public void fromJSON(final InputStream stream) throws ColorScaleException {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));

            StringBuilder builder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line);
            }

            fromJSON(builder.toString());
        } catch (IOException e) {
            e.printStackTrace();
            throw new ColorScaleException(e);
        }

    }

    // load color scale from json
    public void fromJSON(final String json) throws ColorScaleException {
        try {
            final ObjectMapper mapper = new ObjectMapper();
            final Map<String, Object> colorScaleMap = mapper.readValue(json, Map.class);
            final String scalingStr = (String) colorScaleMap.get("Scaling");
            if (scalingStr != null) {
                scaling = Scaling.valueOf(scalingStr);
            }
            final String reliefShadingStr = (String) colorScaleMap.get("ReliefShading");
            reliefShading = ("1").equals(reliefShadingStr) || ("true").equalsIgnoreCase(reliefShadingStr);

            final String interpolateStr = (String) colorScaleMap.get("Interpolate");
            interpolate = (interpolateStr == null) || interpolateStr.isEmpty() || ("1").equals(interpolateStr)
                    || ("true").equalsIgnoreCase(interpolateStr);

            final String forceStr = (String) colorScaleMap.get("ForceValuesIntoRange");
            forceValuesIntoRange = ("1").equals(forceStr) || ("true").equalsIgnoreCase(forceStr);

            final Map<String, String> nullColorMap = (Map<String, String>) colorScaleMap.get("NullColor");
            final String nullColorStr = nullColorMap.get("color");
            if (nullColorStr != null) {
                parseColor(nullColorStr, nullColorMap.get("opacity"), nullColor);
            }

            final ArrayList<Map<String, String>> colorsList = (ArrayList<Map<String, String>>) colorScaleMap
                    .get("Colors");
            if (colorsList != null) {
                for (final Map<String, String> color : colorsList) {
                    final int[] colorArr = new int[4];
                    final String colorStr = color.get("color");
                    final String valueStr = color.get("value");
                    final Double value = Double.valueOf(valueStr);
                    if (colorStr != null) {
                        parseColor(colorStr, color.get("opacity"), colorArr);
                        put(value, colorArr);
                    }
                }
            }
        } catch (final Exception e) {
            throw new BadJSONException(e);
        }
    }

    final public int[] lookup(final double v) {
        if (Double.isNaN(v) || v == transparent) {
            return nullColor;
        }
        if (cache == null) {
            buildCache();
        }

        if (v < min) {
            if (forceValuesIntoRange) {
                return cache[0];
            }
            return nullColor;
        } else if (v > max) {
            if (forceValuesIntoRange) {
                return cache[CACHE_SIZE - 1];
            }
            return nullColor;
        } else {
            final int i = (int) ((v - min) / (max - min) * (CACHE_SIZE - 1) + 0.5);
            // int i = (int) ((v - min) / (max - min) * (CACHE_SIZE - 1));
            return cache[i];
        }
    }

    final public void lookup(final double v, final int[] color) {
        final int[] c = lookup(v);
        System.arraycopy(c, 0, color, 0, 4);
    }

    public void put(final double key, final Color c) {
        put(new Double(key), c);
        cache = null;
    }

    public void put(final double key, final int r, final int g, final int b) {
        put(new Double(key), new Color(r, g, b));
        cache = null;
    }

    public void put(final double key, final int r, final int g, final int b, final int a) {
        put(new Double(key), new Color(r, g, b, a));
        cache = null;
    }

    public void put(final double key, final int[] c) {
        put(new Double(key), new Color(c[0], c[1], c[2], c[3]));
        cache = null;
    }

    /**
     * Method is responsible for generating a color scale from hard coded values. It should be called
     * when there is no input default color scale.
     */
    public void setDefaultGrayScaleValues() {
        clear();
        setScaling(ColorScale.Scaling.MinMax);
        put(0.0, new Color(0, 0, 0));
        put(0.1, new Color(26, 26, 26));
        put(0.2, new Color(52, 52, 52));
        put(0.3, new Color(77, 77, 77));
        put(0.4, new Color(102, 102, 102));
        put(0.5, new Color(128, 128, 128));
        put(0.6, new Color(154, 154, 154));
        put(0.7, new Color(179, 179, 179));
        put(0.8, new Color(205, 205, 205));
        put(0.9, new Color(230, 230, 230));
        put(1.0, new Color(255, 255, 255));
        setInterpolate(true);
        setForceValuesIntoRange(false);
    }

    /**
     * Method is responsible for generating a color scale from hard coded values. It should be called
     * when there is no input default color scale.
     */
    public void setDefaultValues() {
        clear();
        setScaling(ColorScale.Scaling.MinMax);
        put(0.0, new Color(0, 0, 0, 0));
        put(1e-12, new Color(255, 255, 255, 128));
        put(0.1, new Color(247, 252, 253));
        put(0.2, new Color(229, 245, 249));
        put(0.3, new Color(204, 236, 230));
        put(0.4, new Color(153, 216, 201));
        put(0.5, new Color(102, 194, 164));
        put(0.6, new Color(65, 174, 118));
        put(0.7, new Color(35, 139, 69));
        put(0.8, new Color(0, 109, 44));
        put(0.9, new Color(0, 68, 27));
        put(1.0, new Color(0, 0, 0));
        setInterpolate(true);
        setForceValuesIntoRange(false);
    }

    /**
     * Sets the min and max value for scaling. This assumes that the entries in the color scale range
     * from 0 to 1. This also forces the scaling to be min/max. Min will be scaled to 0 and max will
     * be scaled to 1 in the color scale. All values between will be linearly interpolated. Values out
     * of bounds will be forced into range according to the forceValuesIntoRange property, which is
     * false by default.
     *
     * @param min The minimum value to be expected (smaller values will just be forced to min.
     * @param max The maximum value to be expected (larger values will just be forced to max.
     */
    public void setScaleRange(final double min, final double max) {
        this.min = min;
        this.max = max;
        if (scaling != Scaling.Modulo) {
            scaling = Scaling.MinMax;
        }
        cache = null;
        buildCache();
    }

    protected void buildCache() {
        cache = new int[CACHE_SIZE][4];

        if (scaling == Scaling.Absolute) {
            min = firstKey();
            max = lastKey();
        }

        for (int i = 0; i < CACHE_SIZE; i++) {
            final double v = min + (max - min) * ((double) i / (double) (CACHE_SIZE - 1));
            if (interpolate) {
                interpolateValue(v, cache[i]);
            } else {
                absoluteValue(v, cache[i]);
            }
        }
    }

    /**
     * Find the color band that this value falls in and assign that color.
     *
     * @param v
     * @param color
     */
    final private void absoluteValue(final double v, final int[] color) {
        final double search;
        switch (scaling) {
        case Absolute:
            search = v;
            break;
        case MinMax:
            search = (v - min) / (max - min);
            break;
        case Modulo:
            search = v % (max - min);
            break;
        default:
            search = 0;
            break;
        }

        final Map.Entry<Double, Color> lower = floorEntry(search);

        Color c;
        if (lower == null) {
            c = entrySet().iterator().next().getValue();
        } else {
            c = lower.getValue();
        }

        color[R] = c.getRed();
        color[G] = c.getGreen();
        color[B] = c.getBlue();
        color[A] = c.getAlpha();
    }

    /**
     * Interpolate the color value for the given scalar value. The result is placed in color.
     *
     * @param v
     * @param color
     * @return
     */
    final private void interpolateValue(final double v, final int[] color) {
        final double search;
        switch (scaling) {
        case Absolute:
            search = v;
            break;
        case MinMax:
            search = (v - min) / (max - min);
            break;
        case Modulo:
            search = (v - min) % (max - min);
            break;
        default:
            search = 0;
            break;
        }

        final Map.Entry<Double, Color> lower = floorEntry(search);
        final Map.Entry<Double, Color> upper = higherEntry(search);

        assert (upper != null || lower != null);

        if (upper == null) {
            final Color c = lower.getValue();
            color[R] = c.getRed();
            color[G] = c.getGreen();
            color[B] = c.getBlue();
            color[A] = c.getAlpha();
        } else if (lower == null) {
            final Color c = upper.getValue();
            color[R] = c.getRed();
            color[G] = c.getGreen();
            color[B] = c.getBlue();
            color[A] = c.getAlpha();
        } else {
            final double diff = upper.getKey().doubleValue() - lower.getKey().doubleValue();
            final double lw = 1.0 - ((search - lower.getKey().doubleValue()) / diff);
            final double uw = 1.0 - lw;

            final Color lc = lower.getValue();
            final Color uc = upper.getValue();
            color[R] = (int) Math.round(lc.getRed() * lw + uc.getRed() * uw);
            color[G] = (int) Math.round(lc.getGreen() * lw + uc.getGreen() * uw);
            color[B] = (int) Math.round(lc.getBlue() * lw + uc.getBlue() * uw);
            color[A] = (int) Math.round(lc.getAlpha() * lw + uc.getAlpha() * uw);
        }
    }

    public enum Scaling {
        Absolute, MinMax, Modulo
    }

    public static class BadSourceException extends ColorScaleException {
        private static final long serialVersionUID = 1L;

        public BadSourceException(final Exception e) {
            super(e);
        }

    }

    public static class BadJSONException extends ColorScaleException {
        private static final long serialVersionUID = 1L;

        public BadJSONException(final Exception e) {
            super(e);
        }

    }

    public static class BadXMLException extends ColorScaleException {
        private static final long serialVersionUID = 1L;

        public BadXMLException(final Exception e) {
            super(e);
        }

    }

    public static class ColorScaleException extends Exception {
        private static final long serialVersionUID = 1L;
        private final Exception origException;

        public ColorScaleException(final Exception e) {
            this.origException = e;
            printStackTrace();
        }

        public ColorScaleException(final String msg) {
            final Exception e = new Exception(msg);
            this.origException = e;
        }

        @Override
        public void printStackTrace() {
            origException.printStackTrace();
        }

    }
}