/** * Copyright (C) 2010-18 diirt developers. See COPYRIGHT.TXT * All rights reserved. Use is subject to license terms. See LICENSE.TXT */ package org.diirt.graphene; import org.diirt.util.stats.Range; import javafx.scene.paint.Color; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import static java.lang.Double.parseDouble; import static java.lang.Integer.parseInt; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import org.diirt.util.config.Configuration; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; import org.diirt.util.array.ArrayDouble; import org.diirt.util.array.ListDouble; import org.diirt.util.array.ListNumbers; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * Factory and registry class for {@link NumberColorMap}s. * It allows to create and register new maps, and allows a central place to * find registered color maps by name. * * @author carcassi */ public class NumberColorMaps { private static final Logger log = Logger.getLogger(NumberColorMaps.class.getName()); private NumberColorMaps() { // Utility class. Do not instanciate. } /** * Loads a {@code NumberColorMap} from a file. * <p> * It must follow one of the supported format, or an * exception is returned. The extension is used to determine * which file format is being used. * * @param file the color map file * @return the new map */ public static NumberColorMap load(File file) { if (file.getName().endsWith(".xml")) { // Reading from xml return loadXML(file); } else if (file.getName().endsWith(".cmap")) { // Reading from CMAP return loadCMAP(file); } // File format not recognized throw new RuntimeException("File Format not Recognized" + file); } private static NumberColorMap loadCMAP(File file){ List<Color> colors = new ArrayList<>(); Scanner scanner; try { scanner = new Scanner(file); } catch (FileNotFoundException ex) { throw new RuntimeException("Colormap file " + file + " not found", ex); } String line; while (scanner.hasNextLine()) { line = scanner.nextLine(); String[] tokens = line.split(","); if (tokens.length != 3) { throw new RuntimeException("Error Parsing RGB value from file: "+file); } colors.add(Color.rgb(parseInt(tokens[0]), parseInt(tokens[1]), parseInt(tokens[2]),1.0)); } String colormapName = file.getName(); colormapName = colormapName.substring(0,colormapName.lastIndexOf('.')); // cmap file is automatically relative return relative(colors, Color.BLACK, colormapName); } private static NumberColorMap loadXML(File file){ //if we are reading from a xml file List<Double> positions = new ArrayList<>(); List<Color> colors = new ArrayList<>(); boolean relative; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder; Document doc; try { builder = factory.newDocumentBuilder(); } catch (ParserConfigurationException ex) { throw new RuntimeException("Couldn't load color map from file: " + file, ex); } try { doc = builder.parse(file); } catch (SAXException ex) { throw new RuntimeException("Couldn't parse color map file: "+ file, ex); } catch (IOException ex) { throw new RuntimeException("Couldn't load color map from file: "+ file, ex); } Element root = doc.getDocumentElement(); // Throw exception if they don't match format supported if (root.getAttribute("position").equals("relative") || root.getAttribute("position").equals("absolute")){ relative = root.getAttribute("position").equals("relative"); }else{ throw new RuntimeException("Colormap file only supports absoulute and relative scale " + file); } Color nanColor = Color.web(root.getAttribute("colorNaN")); NodeList children = root.getChildNodes(); for(int i = 0 ; i<children.getLength();++i){ Node child = children.item(i); if (child instanceof Element) { Element e = (Element)child; if (e.getTagName().equals("color")) { positions.add(parseDouble( e.getAttribute("position"))); colors.add(Color.web(e.getAttribute("value"))); } } } double[] positionsArray = new double[positions.size()]; for (int i = 0; i < positions.size(); ++i) { positionsArray[i] = positions.get(i); } String colormapName = file.getName(); colormapName = colormapName.substring(0,colormapName.lastIndexOf('.')); return new NumberColorMapGradient(colors, new ArrayDouble(positionsArray), relative, nanColor, colormapName); } private static void initializeColorMapDirectory(File path) { // List of default color maps String [] mapNames = { "BONE.xml", "GRAY.xml", "HOT.xml", "HSV.xml", "HSVRadian.xml", "JET.xml" }; for (String map: mapNames) { File mapFile = new File(path,map); try { mapFile.createNewFile(); } catch (IOException ex) { log.log(Level.WARNING, "Failed Creating new file " + mapFile, ex); continue; } try (InputStream input = NumberColorMaps.class.getResourceAsStream(map); OutputStream output = new FileOutputStream(mapFile)) { byte[] buffer = new byte[8*1024]; int bytesRead; while ((bytesRead = input.read(buffer)) != -1) { output.write(buffer, 0, bytesRead); } } catch (IOException ex) { log.log(Level.WARNING, "Failed Loading " + map, ex); } } } private static List<NumberColorMap> loadMapsFromLocal() { List<NumberColorMap> maps = new ArrayList<>(); File path = new File(Configuration.getDirectory(), "graphene/colormaps"); // If maps are not there, create them first if (!path.exists()) { path.mkdirs(); log.log(Level.CONFIG, "Creating path graphene/colormaps under DIIRT_HOME "); initializeColorMapDirectory(path); } // Load maps from local directory log.log(Level.CONFIG, "Loading ColorMaps from directory: " + path); for (File file : path.listFiles()) { log.log(Level.CONFIG, "Loading ColorMap from file: " + file); try { maps.add(load(file)); } catch (RuntimeException ex) { log.log(Level.WARNING, ex.getMessage()); } } return maps; } // TODO: remove hard-coded color maps. They should be loaded by name // from the registered map /** * JET ranges from blue to red, going through cyan and yellow. */ public static final NumberColorMap JET = relative(Arrays.asList(new Color[]{Color.rgb(0,0,138), Color.BLUE, Color.CYAN, Color.YELLOW, Color.RED, Color.rgb(138,0,0)}), Color.BLACK, "JET"); /** * GRAY ranges from black to white. */ public static final NumberColorMap GRAY= relative(Arrays.asList(new Color[]{Color.BLACK, Color.WHITE }),Color.RED,"GRAY"); /** * BONE ranges from black to white passing from blue. */ public static final NumberColorMap BONE = relative(Arrays.asList(new Color[]{Color.BLACK, Color.rgb(57, 57, 86), Color.rgb(107, 115, 140), Color.rgb(165, 198, 198), Color.WHITE } ),Color.RED,"BONE"); /** * HOT ranges from black to white passing from red and yellow. */ public static final NumberColorMap HOT = relative(Arrays.asList(Color.BLACK, Color.RED, Color.YELLOW, Color.WHITE ), Color.BLUE, "HOT"); /** * HSV goes through the color wheel: red, yellow, green, cyan, blue, magenta * and back to red. Useful for periodic functions. */ public static final NumberColorMap HSV = relative(Arrays.asList(new Color[]{Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.RED } ),Color.BLACK,"HSV"); private static final Map<String, NumberColorMap> registeredColorSchemes = new ConcurrentHashMap<>(); static { List<NumberColorMap> maps = loadMapsFromLocal(); for (NumberColorMap map: maps) { registeredColorSchemes.put(map.toString(),map); } } public static final String DEFAULT_NUMBER_COLOR_MAP_NAME = "JET"; /** * Returns the default {@code NumberColorMap}. It searches for {@link #DEFAULT_NUMBER_COLOR_MAP_NAME} * within the registered maps, and if not found it uses a hard coded version. * * @return a color map; never null */ public static NumberColorMap defaultNumberColorMap() { NumberColorMap colorMap = null; try { colorMap = getRegisteredColorSchemes().get(DEFAULT_NUMBER_COLOR_MAP_NAME); } catch(Exception ex) { // Loading failed } if (colorMap != null) { return colorMap; } else { return JET; } } /** * A set of registered color maps available to all applications. * * @return a set of color maps and their names */ public static Map<String, NumberColorMap> getRegisteredColorSchemes() { return Collections.unmodifiableMap(registeredColorSchemes); } /** * Returns a new optimized instance created by pre-calculating the colors * in the given range and storing them in an array. * <p> * An optimized map will trade off precision for speed. The color will not * change smoothly but will be quantized to the size of the array. * * @param instance the color map instance to optimize * @param range the range of values to optimize * @return the optimized map */ public static NumberColorMapInstance optimize(NumberColorMapInstance instance, Range range){ return new NumberColorMapInstanceOptimized(instance, range); } /** * Creates a new {@code ColorMap} where the color list is equally * spaced. * * @param colors the list of colors used for the values * @param nanColor the color used for NaN values * @param name the name of the color map * @return the new color map */ public static NumberColorMap relative(List<Color> colors, Color nanColor, String name) { return new NumberColorMapGradient(colors, ListNumbers.linearListFromRange(0.0, 1.0, colors.size()), true, nanColor, name); } /** * Creates a new{@code ColorMap} where the color list is spaced out * according to the percentage points specified * @param colors the list of colors used * @param percentages the list of percentages position that divide up the colors * @param nanColor the color used for Nan values * @param name the name of the color map * @return the new color map */ public static NumberColorMap relative(List<Color> colors, ListDouble percentages, Color nanColor, String name) { return new NumberColorMapGradient(colors, percentages, true, nanColor, name); } /** * Creates a new{@code ColorMap} where the color list is spaced out * according to the absolute points specified * @param colors the list of colors used * @param values the list of absolute position that divide up the colors * @param nanColor the color used for Nan values * @param name the name of the color map * @return the new color map */ public static NumberColorMap absolute(List<Color> colors, ListDouble values, Color nanColor, String name) { return new NumberColorMapGradient(colors, values, false, nanColor, name); } }