/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2013, Geomatys
*
* This library 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 2.1 of the License, or (at your option) any later version.
*
* This library 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.
*/
package org.geotoolkit.style.io;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.io.SimpleFileFilter;
import org.geotoolkit.style.MutableStyleFactory;
import org.geotoolkit.style.StyleConstants;
import static org.geotoolkit.style.StyleConstants.DEFAULT_CATEGORIZE_LOOKUP;
import static org.geotoolkit.style.StyleConstants.DEFAULT_FALLBACK;
import org.geotoolkit.style.function.InterpolationPoint;
import org.geotoolkit.style.function.Method;
import org.geotoolkit.style.function.Mode;
import org.geotoolkit.style.function.ThreshholdsBelongTo;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Function;
import org.opengis.style.ColorMap;
/**
* Palette reader based on pattern.
* template :
* - regex ignore line
* (newline)
* - values pattern (v1,r1,g1,b1,v2,r2,g2,b2)
*
* CPT are generated by tools : grd2cpt
* CLR are generated by tools : Argis ColorRamp2.0
* PAL are generated by various tools with various patterns, this is just one of the possibilities.
*
* @author Johann Sorel (Geomatys)
*/
public class PaletteReader {
public static final SimpleFileFilter FILE_FILTER = new SimpleFileFilter("Palette",false,new String[]{"clr","cpt","pal"});
protected static final FilterFactory FF = FactoryFinder.getFilterFactory(null);
protected static final MutableStyleFactory SF = (MutableStyleFactory) FactoryFinder.getStyleFactory(
new Hints(Hints.STYLE_FACTORY, MutableStyleFactory.class));
private class Row implements Comparable<Row>{
Double v1 = null;
Double v2 = null;
Integer r1 = null;
Integer r2 = null;
Integer g1 = null;
Integer g2 = null;
Integer b1 = null;
Integer b2 = null;
@Override
public int compareTo(Row other) {
if(v1==null) return -1;
else if (other.v1 == null) return 1;
return v1.compareTo(other.v1);
}
}
public static final String PATTERN_CLR = "^ColorMap.* \n v1 r1 g1 b1";
public static final String PATTERN_CPT = "^(#|B|F|N).* \n v1 r1 g1 b1 v2 r2 g2 b2";
public static final String PATTERN_PAL = "r1,g1,b1,\"v2 ?- ?v1\"";
private final Pattern[] ignorePatterns;
private final String valuePattern;
private final boolean categorize;
private final Pattern valStart;
/**
*
* @param pattern
*/
public PaletteReader(String pattern) {
final String[] parts = pattern.split("\n");
ignorePatterns = new Pattern[parts.length-1];
for(int i=0,n=parts.length-1;i<n;i++){
ignorePatterns[i] = Pattern.compile(parts[i].trim());
}
valuePattern = parts[parts.length-1].trim();
categorize = valuePattern.contains("v2");
valStart = Pattern.compile("^(v1|v2|r1|r2|g1|g2|b1|b2).*");
}
public ColorMap read(String candidate) throws IOException{
final String[] parts = candidate.split("\n");
final List<Row> rows = new ArrayList<>();
lines:
for(String part : parts){
part = part.trim();
if(part.isEmpty()) continue lines;
//check if we ignore this line
for(Pattern p : ignorePatterns){
if(p.matcher(part).matches()){
continue lines;
}
}
//parse values
String pattern = valuePattern;
final Row row = new Row();
while(!part.isEmpty()){
boolean optional = false;
if(pattern.charAt(0) == '?'){
optional = true;
pattern = pattern.substring(1);
}
if(!valStart.matcher(pattern).matches()){
char c = pattern.charAt(0);
if(c==' '){
part = part.trim();
}else{
char v = part.charAt(0);
if(v==c){
part = part.substring(1);
}else if(!optional){
throw new IOException("Pattern do not match.");
}
}
pattern = pattern.substring(1);
continue;
}
//we work with a value
final int numberEnd = numberEnd(part);
if(numberEnd==0){
pattern = pattern.substring(2);
if(optional){
continue;
}else{
throw new IOException("Pattern do not match.");
}
}
Double val = parseDouble(part, numberEnd);
part = part.substring(numberEnd);
if(pattern.startsWith("v1")) row.v1 = val;
if(pattern.startsWith("v2")) row.v2 = val;
if(pattern.startsWith("r1")) row.r1 = val.intValue();
if(pattern.startsWith("r2")) row.r2 = val.intValue();
if(pattern.startsWith("g1")) row.g1 = val.intValue();
if(pattern.startsWith("g2")) row.g2 = val.intValue();
if(pattern.startsWith("b1")) row.b1 = val.intValue();
if(pattern.startsWith("b2")) row.b2 = val.intValue();
pattern = pattern.substring(2);
}
rows.add(row);
}
//sort values in ascending order
Collections.sort(rows);
final ColorMap colorMap;
if(!categorize){
//interpolated color model
final List<InterpolationPoint> values = new ArrayList<>();
for(Row row : rows){
values.add( SF.interpolationPoint(row.v1, SF.literal(new Color(row.r1,row.g1,row.b1))));
}
final Function function = SF.interpolateFunction(DEFAULT_CATEGORIZE_LOOKUP,
values, Method.COLOR, Mode.LINEAR, DEFAULT_FALLBACK);
colorMap = SF.colorMap(function);
}else{
//categorize color model
final Map<Expression, Expression> values = new HashMap<>();
for(int i=0,n=rows.size();i<n;i++){
final Row row = rows.get(i);
if(values.isEmpty()){
if(row.v1==null){
values.put( StyleConstants.CATEGORIZE_LESS_INFINITY, SF.literal(new Color(row.r1,row.g1,row.b1)));
}else{
//add a translucent range from -infinity to value
values.put( StyleConstants.CATEGORIZE_LESS_INFINITY, SF.literal(new Color(0f,0f,0f,0f)));
values.put( FF.literal(row.v1), SF.literal(new Color(row.r1,row.g1,row.b1)));
}
}else{
//two values, two colors
values.put( FF.literal(row.v1), SF.literal(new Color(row.r1,row.g1,row.b1)));
}
//special case for last element
if(i==n-1){
if(row.r2==null){
values.put( FF.literal(row.v2), SF.literal(new Color(0f,0f,0f,0f)));
}else{
values.put( FF.literal(row.v2), SF.literal(new Color(row.r2,row.g2,row.b2)));
}
}
}
final Function function = SF.categorizeFunction(DEFAULT_CATEGORIZE_LOOKUP,
values, ThreshholdsBelongTo.SUCCEEDING, DEFAULT_FALLBACK);
colorMap = SF.colorMap(function);
}
return colorMap;
}
private static double parseDouble(String candidate, int end){
String str = candidate.substring(0,end);
return Double.parseDouble(str);
}
private static int numberEnd(String candidate) throws IOException{
int end=0;
//possible negation
if(candidate.charAt(0) == '-'){
end++;
}
while(candidate.length()>end && (Character.isDigit(candidate.charAt(end)) || candidate.charAt(end)=='.')){
end++;
}
return end;
}
}