/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009 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;
* version 2.1 of the License.
*
* 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.interval;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
import java.awt.Color;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.table.AbstractTableModel;
import org.apache.sis.internal.feature.AttributeConvention;
import org.geotoolkit.data.FeatureIterator;
import org.geotoolkit.data.query.QueryBuilder;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.map.FeatureMapLayer;
import org.apache.sis.storage.DataStoreException;
import org.geotoolkit.display2d.GO2Utilities;
import org.geotoolkit.style.MutableFeatureTypeStyle;
import org.geotoolkit.style.MutableRule;
import org.geotoolkit.style.MutableStyle;
import org.geotoolkit.style.MutableStyleFactory;
import org.geotoolkit.style.StyleConstants;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.Operation;
import org.opengis.feature.PropertyNotFoundException;
import org.opengis.feature.PropertyType;
import org.opengis.filter.And;
import org.opengis.filter.BinaryComparisonOperator;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
import org.opengis.filter.PropertyIsLessThan;
import org.opengis.filter.PropertyIsLessThanOrEqualTo;
import org.opengis.filter.expression.Divide;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import org.opengis.style.Fill;
import org.opengis.style.Graphic;
import org.opengis.style.GraphicalSymbol;
import org.opengis.style.LineSymbolizer;
import org.opengis.style.Mark;
import org.opengis.style.PointSymbolizer;
import org.opengis.style.PolygonSymbolizer;
import org.opengis.style.Stroke;
import org.opengis.style.Symbolizer;
/**
*
* @author Johann Sorel (Geomatys)
* @module
*/
public class IntervalStyleBuilder extends AbstractTableModel{
private static NumberFormat FORMAT = NumberFormat.getNumberInstance();
public static enum METHOD{
EL,
QANTILE,
MANUAL
};
public final PropertyName noValue;
private final MutableStyleFactory sf;
private final FilterFactory ff;
private FeatureMapLayer layer;
private PropertyName classification;
private PropertyName normalize;
private int nbClasses = 5;
private double[] values = new double[0];
private Double[] allValues = new Double[0];
private METHOD method = METHOD.EL;
private boolean genericAnalyze = false;
private boolean analyze = false;
private final List<PropertyName> properties = new ArrayList<PropertyName>();
private long count = 0;
private double minimum = 0;
private double maximum = 0;
private double sum = 0;
private double mean = 0;
private double median = 0;
private Symbolizer template = null;
public IntervalStyleBuilder() {
this(null,null);
}
public IntervalStyleBuilder(final MutableStyleFactory styleFactory, final FilterFactory filterFactory){
if(styleFactory == null){
sf = (MutableStyleFactory) FactoryFinder.getStyleFactory(new Hints(Hints.STYLE_FACTORY, MutableStyleFactory.class));
}else{
sf = styleFactory;
}
if(filterFactory == null){
ff = FactoryFinder.getFilterFactory(null);
}else{
ff = filterFactory;
}
noValue = ff.property("-");
}
public long getCount() {
analyze();
return count;
}
public double getSum() {
analyze();
return sum;
}
public double getMinimum() {
analyze();
return minimum;
}
public double getMaximum() {
analyze();
return maximum;
}
public double getMean() {
analyze();
return mean;
}
public double getMedian() {
analyze();
return median;
}
public double[] getValues() {
analyze();
return values;
}
public Double[] getAllValues(){
return allValues.clone();
}
public void setValues(final double[] values) {
this.values = values;
}
private void reset(){
analyze = false;
}
public void setLayer(final FeatureMapLayer layer) {
this.layer = layer;
genericAnalyze = false;
reset();
isIntervalStyle(layer.getStyle());
}
public boolean isIntervalStyle(final MutableStyle style){
if(style.featureTypeStyles().size() != 1) return false;
final MutableFeatureTypeStyle fts = style.featureTypeStyles().get(0);
return isIntervalStyle(fts);
}
public boolean isIntervalStyle(final MutableFeatureTypeStyle fts){
if(fts.rules().isEmpty()) return false;
for(MutableRule r : fts.rules()){
Filter f = r.getFilter();
if(f == null || r.isElseFilter()) return false;
if(r.symbolizers().size() != 1) return false;
if(f instanceof And){
And and = (And) f;
if(and.getChildren().size() != 2) return false;
Filter op1 = and.getChildren().get(0);
Filter op2 = and.getChildren().get(1);
if(op2 instanceof PropertyIsGreaterThanOrEqualTo){
//flip order
op1 = op2;
op2 = and.getChildren().get(0);
}
if(op1 instanceof PropertyIsGreaterThanOrEqualTo){
PropertyIsGreaterThanOrEqualTo under = (PropertyIsGreaterThanOrEqualTo) op1;
Expression exp1 = under.getExpression1();
Expression exp2 = under.getExpression2();
if(exp1 instanceof Divide){
Divide div = (Divide) exp1;
if(!properties.contains(div.getExpression1())) return false;
if(!properties.contains(div.getExpression2())) return false;
}else if(exp1 instanceof PropertyName){
PropertyName name = (PropertyName) exp1;
if(!properties.contains(name)) return false;
}else{
return false;
}
if(!(exp2 instanceof Literal)){
return false;
}
if(op2 instanceof PropertyIsLessThan || op2 instanceof PropertyIsLessThanOrEqualTo){
BinaryComparisonOperator bc = (BinaryComparisonOperator)op2;
Expression ex1 = under.getExpression1();
Expression ex2 = under.getExpression2();
if(ex1 instanceof Divide){
Divide div = (Divide) ex1;
if(!properties.contains(div.getExpression1())) return false;
if(!properties.contains(div.getExpression2())) return false;
}else if(ex1 instanceof PropertyName){
PropertyName name = (PropertyName) ex1;
if(!properties.contains(name)) return false;
}else{
return false;
}
if(!(ex2 instanceof Literal)){
return false;
}
}else{
return false;
}
}else{
return false;
}
}
template = r.symbolizers().get(0);
}
method = METHOD.MANUAL;
nbClasses = fts.rules().size()+1;
return true;
}
public FeatureMapLayer getLayer() {
return layer;
}
public void setClassification(final PropertyName classification) {
this.classification = classification;
genericAnalyze = false;
reset();
}
public void setNormalize(final PropertyName normalize) {
this.normalize = normalize;
genericAnalyze = false;
reset();
}
public void setMethod(final METHOD method) {
this.method = method;
reset();
}
public METHOD getMethod() {
return method;
}
public void setNbClasses(final int nbClasses) {
this.nbClasses = nbClasses;
reset();
}
public int getNbClasses() {
return nbClasses;
}
public List<PropertyName> getProperties() {
analyze();
return properties;
}
public Symbolizer getTemplate() {
analyze();
return template;
}
public void setTemplate(final Symbolizer template) {
this.template = template;
}
private void genericAnalyze(){
if(genericAnalyze) return;
genericAnalyze = true;
properties.clear();
minimum = Double.POSITIVE_INFINITY;
maximum = Double.NEGATIVE_INFINITY;
count = 0;
sum = 0;
median = 0;
mean = 0;
//search the different numeric attributs
FeatureType schema = layer.getCollection().getFeatureType();
for(PropertyType desc : schema.getProperties(true)){
if(desc instanceof AttributeType){
Class<?> type = ((AttributeType)desc).getValueClass();
if(Number.class.isAssignableFrom(type) || type == byte.class || type==short.class ||
type==int.class || type==long.class || type==float.class || type == double.class){
properties.add(ff.property(desc.getName().tip().toString()));
}
}
}
//find the geometry class for template
Class<?> geoClass = null;
try{
PropertyType geo = schema.getProperty(AttributeConvention.GEOMETRY_PROPERTY.toString());
geoClass = ((AttributeType)((Operation)geo).getResult()).getValueClass();
}catch(PropertyNotFoundException ex){
}
if(geoClass==null){
return;
}
if(template==null){
if(Polygon.class.isAssignableFrom(geoClass) || MultiPolygon.class.isAssignableFrom(geoClass)){
template = createPolygonTemplate();
}else if(LineString.class.isAssignableFrom(geoClass) || MultiLineString.class.isAssignableFrom(geoClass)){
template = createLineTemplate();
}else{
template = createPointTemplate();
}
}
//search the extreme values
final QueryBuilder query = new QueryBuilder(layer.getCollection().getFeatureType().getName().toString());
if(classification == null || layer == null) return;
if(!properties.contains(classification)) return;
final Set<String> qp = new HashSet<String>();
qp.add(classification.getPropertyName());
if(normalize != null && !normalize.equals(noValue)){
qp.add(normalize.getPropertyName());
}
query.setProperties(qp.toArray(new String[0]));
FeatureIterator features = null;
try{
features = layer.getCollection().subCollection(query.buildQuery()).iterator();
List<Double> values = new ArrayList<Double>();
while(features.hasNext()){
Feature sf = features.next();
count++;
Number classifValue = classification.evaluate(sf, Number.class);
double value;
if(classifValue==null){
//skip null values in analyze
continue;
}
if(normalize == null || normalize.equals(noValue)){
value = classifValue.doubleValue();
}else{
Number normalizeValue = normalize.evaluate(sf, Number.class);
value = classifValue.doubleValue() / normalizeValue.doubleValue();
}
values.add(value);
sum += value;
if(value < minimum){
minimum = value;
}
if(value > maximum){
maximum = value;
}
}
mean = (minimum+maximum) / 2;
//find the median
allValues = values.toArray(new Double[values.size()]);
Arrays.sort(allValues);
if (values.isEmpty()) {
median = 0;
} else if (values.size() % 2 == 0) {
median = (allValues[(allValues.length / 2) - 1] + allValues[allValues.length / 2]) / 2.0;
} else {
median = allValues[allValues.length / 2];
}
}catch(DataStoreException ex){
ex.printStackTrace();
}finally{
if(features != null){
features.close();
}
}
}
private void analyze(){
if(analyze) return;
reset();
genericAnalyze();
analyze = true;
if(method == METHOD.EL){
values = new double[nbClasses+1];
for(int i=0;i<values.length;i++){
values[i] = minimum + (float)i / (values.length-1) * (maximum-minimum);
}
}else if(method == METHOD.QANTILE){
values = new double[nbClasses+1];
for(int i=0;i<values.length;i++){
values[i] = allValues[i*(allValues.length-1)/(values.length-1)];
}
}else{
if(values.length != nbClasses+1){
values = Arrays.copyOf(values, nbClasses+1);
}
}
analyze = true;
}
private Symbolizer createSymbolizer(final IntervalPalette palette,final double step){
return derivateSymbolizer(template, palette.interpolate(step));
}
/**
* Derivate a symbolizer with a new color.
*/
private Symbolizer derivateSymbolizer(final Symbolizer symbol, final Color color){
if(symbol instanceof PolygonSymbolizer){
PolygonSymbolizer ps = (PolygonSymbolizer)symbol;
Fill fill = sf.fill(sf.literal(color),ps.getFill().getOpacity());
return sf.polygonSymbolizer(ps.getName(), ps.getGeometryPropertyName(),
ps.getDescription(), ps.getUnitOfMeasure(),
ps.getStroke(),fill,ps.getDisplacement(),ps.getPerpendicularOffset());
}else if(symbol instanceof LineSymbolizer){
LineSymbolizer ls = (LineSymbolizer) symbol;
Stroke oldStroke = ls.getStroke();
Stroke stroke = sf.stroke(sf.literal(color),oldStroke.getOpacity(),oldStroke.getWidth(),
oldStroke.getLineJoin(),oldStroke.getLineCap(),oldStroke.getDashArray(),oldStroke.getDashOffset());
return sf.lineSymbolizer(ls.getName(), ls.getGeometryPropertyName(),
ls.getDescription(), ls.getUnitOfMeasure(), stroke, ls.getPerpendicularOffset());
}else if(symbol instanceof PointSymbolizer){
PointSymbolizer ps = (PointSymbolizer) symbol;
Graphic oldGraphic = ps.getGraphic();
Mark oldMark = (Mark) oldGraphic.graphicalSymbols().get(0);
Fill fill = sf.fill(sf.literal(color),oldMark.getFill().getOpacity());
List<GraphicalSymbol> symbols = new ArrayList<GraphicalSymbol>();
symbols.add(sf.mark(oldMark.getWellKnownName(), fill, oldMark.getStroke()));
Graphic graphic = sf.graphic(symbols, oldGraphic.getOpacity(),oldGraphic.getSize(),
oldGraphic.getRotation(),oldGraphic.getAnchorPoint(),oldGraphic.getDisplacement());
return sf.pointSymbolizer(graphic,ps.getGeometryPropertyName());
}else{
throw new IllegalArgumentException("unexpected symbolizer type : " + symbol);
}
}
public List<MutableRule> generateRules(final IntervalPalette palette){
return generateRules(palette, Filter.INCLUDE);
}
public List<MutableRule> generateRules(final IntervalPalette palette, Filter combinedFilter){
analyze();
List<MutableRule> rules = new ArrayList<MutableRule>();
final Expression exp;
if(normalize == null || normalize.equals(noValue)){
exp = classification;
}else{
exp = ff.divide(classification, normalize);
}
for(int i=1;i<values.length;i++){
final MutableRule rule = sf.rule();
double start = values[i-1];
double end = values[i];
//create the filter and title
final Filter interval;
final String title;
if(i == values.length-1){
//last element
Filter above = ff.greaterOrEqual(exp, ff.literal(start));
Filter under = ff.lessOrEqual(exp, ff.literal(end));
interval = ff.and(above, under);
title = "[ " + FORMAT.format(start) + " -> " + FORMAT.format(end) + " ]";
}else{
Filter above = ff.greaterOrEqual(exp, ff.literal(start));
Filter under = ff.less(exp, ff.literal(end));
interval = ff.and(above, under);
title = "[ " + FORMAT.format(start) + " -> " + FORMAT.format(end) + " [";
}
if(Filter.INCLUDE.equals(combinedFilter) || combinedFilter==null){
rule.setFilter(interval);
}else{
rule.setFilter(ff.and(combinedFilter,interval));
}
rule.setName(title);
rule.setDescription(sf.description(title, title));
//create the style
Symbolizer symbol = createSymbolizer(palette,(double)(i-1)/(values.length-2));
rule.symbolizers().add(symbol);
rules.add(rule);
}
return rules;
}
////////////// TABLE MODEL /////////////////////////////////////////////////
@Override
public int getRowCount() {
return nbClasses;
}
@Override
public int getColumnCount() {
return 1;
}
@Override
public Object getValueAt(final int rowIndex, final int columnIndex) {
analyze();
return values[rowIndex];
}
@Override
public boolean isCellEditable(final int rowIndex, final int columnIndex) {
return method == METHOD.MANUAL;
}
public static PointSymbolizer createPointTemplate(){
final MutableStyleFactory sf = GO2Utilities.STYLE_FACTORY;
final FilterFactory ff = GO2Utilities.FILTER_FACTORY;
final Stroke stroke = sf.stroke(Color.BLACK, 1);
final Fill fill = sf.fill(Color.BLUE);
final List<GraphicalSymbol> symbols = new ArrayList<GraphicalSymbol>();
symbols.add(sf.mark(StyleConstants.MARK_CIRCLE, fill, stroke));
final Graphic gra = sf.graphic(symbols, ff.literal(1), ff.literal(12), ff.literal(0), sf.anchorPoint(), sf.displacement());
return sf.pointSymbolizer(gra, null);
}
public static LineSymbolizer createLineTemplate(){
final MutableStyleFactory sf = GO2Utilities.STYLE_FACTORY;
final FilterFactory ff = GO2Utilities.FILTER_FACTORY;
final Stroke stroke = sf.stroke(Color.BLUE, 2);
return sf.lineSymbolizer(stroke,null);
}
public static PolygonSymbolizer createPolygonTemplate(){
final MutableStyleFactory sf = GO2Utilities.STYLE_FACTORY;
final FilterFactory ff = GO2Utilities.FILTER_FACTORY;
final Stroke stroke = sf.stroke(Color.BLACK, 1);
final Fill fill = sf.fill(Color.BLUE);
return sf.polygonSymbolizer(stroke,fill,null);
}
}