/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.brewer.color; import java.awt.Color; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.Filters; import org.geotools.filter.IllegalFilterException; import org.geotools.filter.function.Classifier; import org.geotools.filter.function.ExplicitClassifier; import org.geotools.filter.function.RangedClassifier; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.Fill; import org.geotools.styling.Graphic; import org.geotools.styling.Mark; import org.geotools.styling.Rule; import org.geotools.styling.Stroke; import org.geotools.styling.StyleBuilder; import org.geotools.styling.StyleFactory; import org.geotools.styling.Symbolizer; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.filter.And; import org.opengis.filter.BinaryComparisonOperator; import org.opengis.filter.BinaryLogicOperator; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.Or; import org.opengis.filter.PropertyIsEqualTo; import org.opengis.filter.PropertyIsGreaterThan; import org.opengis.filter.PropertyIsGreaterThanOrEqualTo; import org.opengis.filter.PropertyIsLessThan; import org.opengis.filter.PropertyIsLessThanOrEqualTo; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.PropertyName; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * Generates a style/featureTypeStyle using ColorBrewer. * <br> * WARNING: this is unstable and subject to radical change. * * @author Cory Horner, Refractions Research Inc. * * @source $URL$ */ public class StyleGenerator { private static final java.util.logging.Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.brewer.color"); public final static int ELSEMODE_IGNORE = 0; public final static int ELSEMODE_INCLUDEASMIN = 1; public final static int ELSEMODE_INCLUDEASMAX = 2; private static FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); private static StyleFactory sf = CommonFactoryFinder.getStyleFactory(null); private static StyleBuilder sb = new StyleBuilder(sf, ff); protected StyleGenerator() { } /** * Obtains the colour for the indexed rule. If an else rule is also to be * created from the colour palette, the appropriate offset is applied. * * @param index */ private static Color getColor(int elseMode, Color[] colors, int index) { if (elseMode == ELSEMODE_IGNORE) { return colors[index]; } else if (elseMode == ELSEMODE_INCLUDEASMIN) { return colors[index + 1]; } else if (elseMode == ELSEMODE_INCLUDEASMAX) { return colors[index]; } else { return null; } } private static Color getElseColor(int elseMode, Color[] colors) { if (elseMode == ELSEMODE_INCLUDEASMIN) { return colors[0]; } else if (elseMode == ELSEMODE_INCLUDEASMAX) { return colors[colors.length - 1]; } else { return null; } } /** * Merges a classifier, array of colors and other data into a * FeatureTypeStyle object. Yes, this constructor is insane and likely to * change very soon. * * @param classifier * @param colors * @param typeId * semantic type identifier, which will be prefixed with * "colorbrewer:" * @param geometryAttrType * @param elseMode * @param opacity * @param defaultStroke * @return * @throws IllegalFilterException */ public static FeatureTypeStyle createFeatureTypeStyle(Classifier classifier, Expression expression, Color[] colors, String typeId, GeometryDescriptor geometryAttrType, int elseMode, double opacity, Stroke defaultStroke) throws IllegalFilterException { //init nulls if (defaultStroke == null) { defaultStroke = sb.createStroke(); } //answer goes here FeatureTypeStyle fts = sf.createFeatureTypeStyle(); // update the number of classes int numClasses = classifier.getSize(); // if (elseMode == ELSEMODE_IGNORE) { // numClasses++; // } //numeric if (classifier instanceof RangedClassifier) { RangedClassifier ranged = (RangedClassifier) classifier; Object localMin = null; Object localMax = null; // for each class for (int i = 0; i < ranged.getSize(); i++) { // obtain min/max values localMin = ranged.getMin(i); localMax = ranged.getMax(i); Rule rule = createRuleRanged(ranged, expression, localMin, localMax, geometryAttrType, i, elseMode, colors, opacity, defaultStroke); fts.addRule(rule); } } else if (classifier instanceof ExplicitClassifier) { ExplicitClassifier explicit = (ExplicitClassifier) classifier; // for each class for (int i = 0; i < explicit.getSize(); i++) { Set value = (Set) explicit.getValues(i); Rule rule = createRuleExplicit(explicit, expression, value, geometryAttrType, i, elseMode, colors, opacity, defaultStroke); fts.addRule(rule); } } else { LOGGER.log(Level.SEVERE, "Error: no handler for this Classifier type"); } // add an else rule to capture any missing features? if (elseMode != ELSEMODE_IGNORE) { Symbolizer symb = createSymbolizer(geometryAttrType, getElseColor(elseMode, colors), opacity, defaultStroke); Rule elseRule = sb.createRule(symb); elseRule.setIsElseFilter(true); elseRule.setTitle("Else"); elseRule.setName("else"); fts.addRule(elseRule); } // sort the FeatureTypeStyle rules Rule[] rule = fts.getRules(); if (elseMode == ELSEMODE_INCLUDEASMIN) { //move last rule to the front for (int i = rule.length - 1; i > 0; i--) { Rule tempRule = rule[i]; rule[i] = rule[i - 1]; rule[i - 1] = tempRule; } } //our syntax will be: ColorBrewer:id fts.setSemanticTypeIdentifiers(new String[] { "generic:geometry", "colorbrewer:" + typeId }); return fts; } /** * Creates a symbolizer for the given geometry * * @param sb * @param geometryAttrType * @param color * @param opacity * @param defaultStroke stroke used for borders * */ private static Symbolizer createSymbolizer(GeometryDescriptor geometryAttrType, Color color, double opacity, Stroke defaultStroke) { Symbolizer symb; if (defaultStroke == null) { defaultStroke = sb.createStroke(color, 1, opacity); } if ((geometryAttrType.getType().getBinding() == MultiPolygon.class) || (geometryAttrType.getType().getBinding() == Polygon.class)) { Fill fill = sb.createFill(color, opacity); symb = sb.createPolygonSymbolizer(defaultStroke, fill); } else if (geometryAttrType.getType().getBinding() == LineString.class) { symb = sb.createLineSymbolizer(color); } else if ((geometryAttrType.getType().getBinding() == MultiPoint.class) || (geometryAttrType.getType().getBinding() == Point.class)) { Fill fill = sb.createFill(color, opacity); Mark square = sb.createMark(StyleBuilder.MARK_SQUARE, fill, defaultStroke); Graphic graphic = sb.createGraphic(null, square, null); //, 1, 4, 0); symb = sb.createPointSymbolizer(graphic); //TODO: handle Text and Raster } else { //we don't know what the heck you are, *snip snip* you're a line. symb = sb.createLineSymbolizer(color); } return symb; } /** * Truncates an unneeded trailing decimal zero (1.0 --> 1) by converting to * an Integer object. * * @param value * * @return Integer(value) if applicable */ private static Object chopInteger(Object value) { if ((value instanceof Number) && (value.toString().endsWith(".0"))) { return new Integer(((Number) value).intValue()); } else { return value; } } /** * Generates a quick name for each rule with a leading zero. * * @param count * */ private static String getRuleName(int count) { String strVal = new Integer(count).toString(); if (strVal.length() == 1) { return "rule0" + strVal; } else { return "rule" + strVal; } } private static Rule createRuleRanged(RangedClassifier classifier, Expression expression, Object localMin, Object localMax, GeometryDescriptor geometryAttrType, int i, int elseMode, Color[] colors, double opacity, Stroke defaultStroke) throws IllegalFilterException { // 1.0 --> 1 // (this makes our styleExpressions more readable. Note that the // filter always converts to double, so it doesn't care what we // do). localMin = chopInteger(localMin); localMax = chopInteger(localMax); // generate a title String title = classifier.getTitle(i); // construct filters Filter filter = null; if (localMin == localMax) { // build filter: = filter = ff.equals(expression, ff.literal(localMax)); } else { // build filter: [min <= x] AND [x < max] Filter lowBoundFilter = null; Filter hiBoundFilter = null; if(localMin != null) { lowBoundFilter = ff.greaterOrEqual(expression, ff.literal(localMin)); } if(localMax != null) { // if this is the global maximum, include the max value if (i == (classifier.getSize() - 1)) { hiBoundFilter = ff.lessOrEqual(expression, ff.literal(localMax)); } else { hiBoundFilter = ff.less(expression, ff.literal(localMax)); } } if ((localMin != null) && (localMax != null)) { filter = ff.and(lowBoundFilter, hiBoundFilter); } else if ((localMin == null) && (localMax != null)) { filter = hiBoundFilter; } else if ((localMin != null) && (localMax == null)) { filter = lowBoundFilter; } } // create a symbolizer Symbolizer symb = createSymbolizer(geometryAttrType, getColor(elseMode, colors, i), opacity, defaultStroke); // create a rule Rule rule = sb.createRule(symb); rule.setFilter(filter); rule.setTitle(title); rule.setName(getRuleName(i + 1)); return rule; } private static Rule createRuleExplicit(ExplicitClassifier explicit, Expression expression, Set value, GeometryDescriptor geometryAttrType, int i, int elseMode, Color[] colors, double opacity, Stroke defaultStroke) { // create a sub filter for each unique value, and merge them // into the logic filter Object[] items = value.toArray(); Arrays.sort(items); String title = ""; List<Filter> filters = new ArrayList<Filter>(); for (int item = 0; item < items.length; item++) { Filter filter; if (items[item] == null) { filter = ff.isNull(expression); } else { filter = ff.equals(expression, ff.literal(items[item])); } // add to the title if (items[item] == null) { title += "NULL"; } else { title += items[item].toString(); } if ((item + 1) != items.length) { title += ", "; } filters.add(filter); } // create the symbolizer Symbolizer symb = createSymbolizer(geometryAttrType, getColor(elseMode, colors, i), opacity, defaultStroke); // create the rule Rule rule = sb.createRule(symb); if (filters.size() == 1){ rule.setFilter(filters.get(0)); }else if (filters.size() > 1){ rule.setFilter(ff.or(filters)); } rule.setTitle(title); rule.setName(getRuleName(i + 1)); return rule; } /** * Used to update an existing style based on the provided input. * * @param fts * @param ruleIndex * @param styleExpression * @throws IllegalFilterException */ public static void modifyFTS(FeatureTypeStyle fts, int ruleIndex, String styleExpression) throws IllegalFilterException { Rule[] rule = fts.getRules(); Rule thisRule = rule[ruleIndex]; Filter filter = thisRule.getFilter(); if (filter instanceof And) { //ranged expression //figure out the appropriate values String[] newValue = styleExpression.split("\\.\\."); //$NON-NLS-1$ if (newValue.length != 2) { throw new IllegalArgumentException( "StyleExpression has incorrect syntax; min..max expected."); } List<Filter> children = ((BinaryLogicOperator) filter).getChildren(); if (children.size() > 2) { throw new IllegalArgumentException( "This method currently only supports logical filters with exactly 2 children."); } // we're expecting 2 compare subfilters PropertyIsGreaterThanOrEqualTo filter1 = (PropertyIsGreaterThanOrEqualTo) children.get(0); BinaryComparisonOperator filter2 = (BinaryComparisonOperator) children.get(1); //filter1 should be 1 <= x and filter2 should be x <(=) 5 if (!(filter1.getExpression2().equals(filter2.getExpression1()))) { throw new IllegalArgumentException( "Subfilters or subExpressions in incorrect order"); } if (filter1.getExpression1().toString() != newValue[0]) { //lower bound value has changed, update filter1 = ff.greaterOrEqual(filter1.getExpression1(), ff.literal(newValue[0])); } if (filter2.getExpression2().toString() != newValue[1]) { //upper bound value has changed, update if(filter2 instanceof PropertyIsLessThan) { filter2 = ff.less(filter1.getExpression1(), ff.literal(newValue[1])); } else if(filter2 instanceof PropertyIsLessThanOrEqualTo) { filter2 = ff.lessOrEqual(filter1.getExpression1(), ff.literal(newValue[1])); } else { throw new IllegalArgumentException("Filter 2 in the comparison is not less or less or equal??"); } } thisRule.setFilter(filter); // style events don't handle filters yet, so fire the change event for filter //TODO: adjust the previous and next filters (uses isFirst, isLast) } else if (filter instanceof Or || filter instanceof PropertyIsEqualTo) { // explicit expression obtain the expression containing the attribute Expression attrExpression; if (filter instanceof Or) { attrExpression = ((BinaryComparisonOperator) ((Or) filter).getChildren().get(0)).getExpression1(); } else { //COMPARE_EQUALS (simple explicit expression) attrExpression = ((PropertyIsEqualTo) filter).getExpression1(); } //recreate the filter with the new values rule[ruleIndex].setFilter(toExplicitFilter(styleExpression, attrExpression)); //TODO: remove duplicate values from other filters } else { throw new IllegalArgumentException("Unrecognized filter type."); } } public static String toStyleExpression(Filter filter) { short filterType = Filters.getFilterType(filter); if (filter instanceof And) { //looks like a ranged filter return toRangedStyleExpression((And) filter); } else { //it's probably a filter with explicitly defined values return toExplicitStyleExpression(filter); } } public static String[] toStyleExpression(Filter[] filter) { String[] styleExpression = new String[filter.length]; for (int i = 0; i < filter.length; i++) { styleExpression[i] = toStyleExpression(filter[i]); } return styleExpression; } /** * <p> * Converts an array of styleExpressions and attributes into Filters * </p> * <p> * <code>styleExpression[0] = "1..5";</code><br> * <code>styleExpression[1] = "5..10";</code><br> * <code>styleExpression[2] = "11, -13";</code><br> * <code>---></code><br> * <code>filter[0] = [[1 <= attr] AND [attr < 5]]</code><br> * <code>filter[1] = [[6 <= attr] AND [attr <= 10]]</code><br> * <code>filter[2] = [[attr = 11] OR [attr = -13]]</code> * </p> * * @param styleExpression * strings of ranged expressions "lowValue..highValue" or * explicit values "value1, value2" * @param featureType * @param attributeTypeName * @return an array with all the filters * @throws IllegalFilterException */ public static Filter[] toFilter(String[] styleExpression, SimpleFeatureType[] featureType, String[] attributeTypeName) throws IllegalFilterException { Filter[] filter = new Filter[styleExpression.length]; // prepare the styleExpressions (fix out if they are ranged, and if so // their min and max values too boolean[] isRangedExpr = new boolean[styleExpression.length]; List<String> min = new ArrayList<String>(); String[] max = new String[styleExpression.length]; for (int i = 0; i < styleExpression.length; i++) { if (isRanged(styleExpression[i])) { isRangedExpr[i] = true; String[] exprPart = styleExpression[i].split("\\.\\."); //$NON-NLS-1$ min.add(exprPart[0]); max[i] = exprPart[1]; } else { isRangedExpr[i] = false; } } // create each filter for (int i = 0; i < styleExpression.length; i++) { // is it ranged or specific? if (isRangedExpr[i]) { boolean upperBoundClosed = true; // check for lower bounds of the same value as the current upper // bound if (min.contains(max[i])) { upperBoundClosed = false; } filter[i] = toRangedFilter(styleExpression[i], featureType[i], attributeTypeName[i], upperBoundClosed); } else { // specific filter[i] = toExplicitFilter(styleExpression[i], featureType[i], attributeTypeName[i]); } } return filter; } /** * <p> * Creates a filter for a range of values. * </p> * <p> * Examples:<br> * "1..5", closed=true --> [[1 <= attr] AND [attr <= 5]]<br> * "1..10", closed=false --> [[1 <= attr] AND [attr < 10]] * "..10, closed=true --> [attr <= 10] * </p> * * @param styleExpression * the ranged style expression (minValue..maxValue) * @param featureType * the featureType * @param attributeTypeName * the attributeTypeName whose values correspond to * @param upperBoundClosed * does the upper bound include the max value? (true: <=, false: <) * @return a filter * @throws IllegalFilterException */ public static Filter toRangedFilter(String styleExpression, SimpleFeatureType featureType, String attributeTypeName, boolean upperBoundClosed) throws IllegalFilterException { PropertyName attrib = ff.property(attributeTypeName); String[] strs = styleExpression.split("\\.\\."); //$NON-NLS-1$ if (strs.length != 2) { throw new IllegalArgumentException( "A ranged filter could not be created from the styleExpression given."); } Literal localMin = ff.literal(strs[0]); Literal localMax = ff.literal(strs[1]); Filter lowerBound = ff.lessOrEqual(localMin, localMax); Filter upperBound; if (upperBoundClosed) { upperBound = ff.lessOrEqual(attrib, localMax); } else { upperBound = ff.less(attrib, localMax); } return ff.and(lowerBound, upperBound); } /** * <p>Converts a filter into a styleExpression with ranged values.</p> * <p>Example:<br> * <code>[[1 <= attr] AND [attr < 5]] --> "1..5"</code></p> * * @param filter A LOGIC_AND filter containing 2 CompareFilters or a single CompareFilter. * @return a styleExpression of the syntax "min..max" */ private static String toRangedStyleExpression(Filter filter) { if (filter instanceof BinaryLogicOperator) { BinaryLogicOperator lFilter = (BinaryLogicOperator) filter; if (!(filter instanceof And)) { throw new IllegalArgumentException( "Only logic filters constructed using the LOGIC_AND filterType are currently supported by this method."); } List<Filter> children = lFilter.getChildren(); // we're expecting 2 subfilters Filter filter1 = children.get(0); Filter filter2 = children.get(1); if (children.size() > 2) { throw new IllegalArgumentException( "This method currently only supports logical filters with exactly 2 children."); } if (!(filter1 instanceof BinaryComparisonOperator) || !(filter2 instanceof BinaryComparisonOperator)) { throw new IllegalArgumentException( "Only compare filters as logical filter children are currently supported by this method."); } // find min and max values Expression min1; Expression min2; Expression max1; Expression max2; if (filter1 instanceof PropertyIsLessThanOrEqualTo || filter1 instanceof PropertyIsLessThan) { min1 = ((BinaryComparisonOperator) filter1).getExpression1(); max1 = ((BinaryComparisonOperator) filter1).getExpression2(); } else if (filter1 instanceof PropertyIsGreaterThanOrEqualTo || filter1 instanceof PropertyIsGreaterThan) { min1 = ((BinaryComparisonOperator) filter1).getExpression2(); max1 = ((BinaryComparisonOperator) filter1).getExpression1(); } else { throw new IllegalArgumentException("Unsupported FilterType"); } if (filter2 instanceof PropertyIsLessThanOrEqualTo || filter2 instanceof PropertyIsLessThan) { min2 = ((BinaryComparisonOperator) filter2).getExpression1(); max2 = ((BinaryComparisonOperator) filter2).getExpression2(); } else if (filter2 instanceof PropertyIsGreaterThanOrEqualTo || filter2 instanceof PropertyIsGreaterThan) { min2 = ((BinaryComparisonOperator) filter2).getExpression2(); max2 = ((BinaryComparisonOperator) filter2).getExpression1(); } else { throw new IllegalArgumentException("Unsupported FilterType"); } //look for 2 equal expressions if (max1.equals(min2)) { return min1.toString() + ".." + max2.toString(); } else if (max2.equals(min1)) { return min2.toString() + ".." + max1.toString(); } else { throw new IllegalArgumentException( "Couldn't find the expected arrangement of Expressions"); } } else if (filter instanceof BinaryComparisonOperator) { // what the heck?? } throw new UnsupportedOperationException("Don't know how to handle this filter"); } /** * Determines if a string is an instance of a ranged expression or unique values. */ public static boolean isRanged(String styleExpression) { return styleExpression.matches(".+\\.{2}.+"); } /** * <p> * Creates a filter with each value explicitly defined. * </p> * <p> * Examples:<br> * "LIB" --> [PARTY = LIB]<br> * "LIB, NDP" --> [[PARTY = LIB] OR [PARTY = NDP]] * </p> * * @param styleExpression * the list of attribute values, separated by commas (and * optional spaces) * @param attributeTypeName * A Sting with the attributeTypeName whose values correspond to * @return a filter * @throws IllegalFilterException */ public static Filter toExplicitFilter(String styleExpression, SimpleFeatureType featureType, String attributeTypeName) throws IllegalFilterException { // eliminate spaces after commas String expr = styleExpression.replaceAll(",\\s+", ","); //$NON-NLS-1$//$NON-NLS-2$ String[] attribValue = expr.split(","); //$NON-NLS-1$ // create the first filter PropertyName attribExpr = ff.property(attributeTypeName); PropertyIsEqualTo cFilter = ff.equals(attribExpr, ff.literal(attribValue[0])); if (attribValue.length == 1) { return cFilter; } // more than one value exists, so wrap them inside a logical OR List<Filter> filters = new ArrayList<Filter>(); filters.add(cFilter); for (int i = 1; i < attribValue.length; i++) { cFilter = ff.equals(attribExpr, ff.literal(attribValue[i])); filters.add(cFilter); } return ff.or(filters); } /** * <p> * Creates a filter with each value explicitly defined. * </p> * <p> * Examples:<br> * "LIB" --> [PARTY = LIB]<br> * "LIB, NDP" --> [[PARTY = LIB] OR [PARTY = NDP]] * </p> * * @param styleExpression * the list of attribute values, separated by commas (and * optional spaces) * @param attribExpr * an Expression to compare each value with (simple case = attributeExpression) * @return a filter * @throws IllegalFilterException */ public static Filter toExplicitFilter(String styleExpression, Expression attribExpr) throws IllegalFilterException { // eliminate spaces after commas String expr = styleExpression.replaceAll(",\\s+", ","); //$NON-NLS-1$//$NON-NLS-2$ String[] attribValue = expr.split(","); //$NON-NLS-1$ // create the first filter PropertyIsEqualTo cFilter = ff.equals(attribExpr, ff.literal(attribValue[0])); if (attribValue.length == 1) { return cFilter; } // more than one value exists, so wrap them inside a logical OR List<Filter> filters = new ArrayList<Filter>(); filters.add(cFilter); for (int i = 1; i < attribValue.length; i++) { cFilter = ff.equals(attribExpr, ff.literal(attribValue[i])); filters.add(cFilter); } return ff.or(filters); } /** * <p> * Converts a filter into a styleExpression with explicitly defined values. * </p> * <p> * Example:<br> * <code>[[attr = 49] OR [attr = 92]] --> "49, 92"</code> * </p> * * @param filter */ private static String toExplicitStyleExpression(Filter filter) { String styleExpression = ""; if (filter instanceof PropertyIsEqualTo) { // figure out which side is the attributeExpression, and which side // is the LiteralExpression PropertyIsEqualTo compareFilter = (PropertyIsEqualTo) filter; Expression leftExpression = compareFilter.getExpression1(); Expression rightExpression = compareFilter.getExpression2(); if ((leftExpression instanceof PropertyName) && (rightExpression instanceof Literal)) { styleExpression = rightExpression.toString(); } else if ((leftExpression instanceof Literal) && (rightExpression instanceof PropertyName)) { styleExpression = leftExpression.toString(); } else { throw new IllegalArgumentException( "Could not extract an Explicit Style Expression from the CompareFilter"); } } else if (filter instanceof Or) { // descend into the child elements of this filter Or parentFilter = (Or) filter; Iterator iterator = parentFilter.getChildren().iterator(); while (iterator.hasNext()) { // recursive call styleExpression += toExplicitStyleExpression((Filter) iterator.next()); if (iterator.hasNext()) { styleExpression += ", "; } } } return styleExpression; } }