/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.featureinfo;
import java.awt.Color;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.geoserver.wms.SymbolizerFilteringVisitor;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.Fill;
import org.geotools.styling.Graphic;
import org.geotools.styling.LineSymbolizer;
import org.geotools.styling.PointSymbolizer;
import org.geotools.styling.PolygonSymbolizer;
import org.geotools.styling.Rule;
import org.geotools.styling.RuleImpl;
import org.geotools.styling.Stroke;
import org.geotools.styling.Style;
import org.geotools.styling.StyleBuilder;
import org.geotools.styling.Symbolizer;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* Removes text symbolizers, makes sure lines and polygons are painted at least with a solid color
* to ensure we match even when hitting in spaces between dashes or spaced fills
*
* @author Andrea Aime - GeoSolutions
*/
class FeatureInfoStylePreprocessor extends SymbolizerFilteringVisitor {
StyleBuilder sb = new StyleBuilder();
FeatureType schema;
Set<Expression> geometriesOnPolygonSymbolizer = new HashSet<Expression>();
Set<Expression> geometriesOnLineSymbolizer = new HashSet<Expression>();
Set<Expression> geometriesOnPointSymbolizer = new HashSet<Expression>();
Set<Expression> geometriesOnTextSymbolizer = new HashSet<Expression>();
Set<Rule> extraRules = new HashSet<Rule>();
private PropertyName defaultGeometryExpression;
private boolean addSolidLineSymbolier;
public FeatureInfoStylePreprocessor(FeatureType schema) {
this.schema = schema;
this.defaultGeometryExpression = ff.property("");
}
public void visit(org.geotools.styling.TextSymbolizer ts) {
pages.push(null);
addGeometryExpression(ts.getGeometry(), geometriesOnTextSymbolizer);
}
@Override
public void visit(Fill fill) {
super.visit(fill);
Fill copy = (Fill) pages.peek();
if (copy.getGraphicFill() != null) {
copy.setGraphicFill(null);
copy.setColor(sb.colorExpression(Color.BLACK));
}
}
/**
* Force a solid color, otherwise we might decide the user did not click on the polygon because
* the area in which he clicked is fully transparent
*/
@Override
public void visit(PolygonSymbolizer poly) {
super.visit(poly);
PolygonSymbolizer copy = (PolygonSymbolizer) pages.peek();
Fill fill = copy.getFill();
if (fill == null || isStaticTransparentFill(fill)) {
copy.setFill(sb.createFill());
}
Stroke stroke = copy.getStroke();
addStrokeSymbolizerIfNecessary(stroke);
addGeometryExpression(poly.getGeometry(), geometriesOnPolygonSymbolizer);
}
private boolean isStaticTransparentFill(Fill fill) {
if (fill.getOpacity() instanceof Literal) {
// weird case of people setting opacity to 0. In case the opacity is really attribute driven,
// we'll leave it be
Double staticOpacity = fill.getOpacity().evaluate(null, Double.class);
if (staticOpacity == null || staticOpacity == 0) {
return true;
}
}
return false;
}
@Override
public void visit(LineSymbolizer line) {
super.visit(line);
LineSymbolizer copy = (LineSymbolizer) pages.peek();
Stroke stroke = copy.getStroke();
addStrokeSymbolizerIfNecessary(stroke);
addGeometryExpression(line.getGeometry(), geometriesOnLineSymbolizer);
}
@Override
public void visit(PointSymbolizer ps) {
super.visit(ps);
addGeometryExpression(ps.getGeometry(), geometriesOnPointSymbolizer);
}
private void addGeometryExpression(Expression geometry,
Set<Expression> expressions) {
if(isDefaultGeometry(geometry)) {
expressions.add(defaultGeometryExpression);
} else {
expressions.add(geometry);
}
}
private boolean isDefaultGeometry(Expression geometry) {
if(geometry == null) {
return true;
}
if(!(geometry instanceof PropertyName)) {
return false;
}
PropertyName pn = (PropertyName) geometry;
if("".equals(pn.getPropertyName())) {
return true;
}
GeometryDescriptor gd = schema.getGeometryDescriptor();
if(gd == null) {
return false;
}
return gd.getLocalName().equals(pn.getPropertyName());
}
@Override
public void visit(Style style) {
super.visit(style);
Style copy = (Style) pages.peek();
// merge the feature type styles sharing the same transformation
List<FeatureTypeStyle> featureTypeStyles = copy.featureTypeStyles();
List<FeatureTypeStyle> reduced = new ArrayList<FeatureTypeStyle>();
FeatureTypeStyle current = null;
for (FeatureTypeStyle fts : featureTypeStyles) {
if(current == null || !sameTranformation(current.getTransformation(), fts.getTransformation())) {
current = fts;
reduced.add(current);
} else {
// flatten, we don't need to draw a pretty picture and having multiple FTS
// would result in the feature being returned twice, since we cannot
// assume feature ids to be stable either
current.rules().addAll(fts.rules());
}
}
// replace
copy.featureTypeStyles().clear();
copy.featureTypeStyles().addAll(reduced);
}
private boolean sameTranformation(Expression t1, Expression t2) {
return (t1 == null && t2 == null) || (t1 != null && t1.equals(t2));
}
@Override
public void visit(FeatureTypeStyle fts) {
extraRules.clear();
super.visit(fts);
if(extraRules.size() > 0) {
FeatureTypeStyle copy = (FeatureTypeStyle) pages.peek();
copy.rules().addAll(extraRules);
}
}
@Override
public void visit(Rule rule) {
geometriesOnLineSymbolizer.clear();
geometriesOnPolygonSymbolizer.clear();
geometriesOnPointSymbolizer.clear();
geometriesOnTextSymbolizer.clear();
addSolidLineSymbolier = false;
super.visit(rule);
Rule copy = (Rule) pages.peek();
if (addSolidLineSymbolier) {
// add also a black line to make sure we get something in output even
// if the user clicks in between symbols or dashes
LineSymbolizer ls = sb.createLineSymbolizer(Color.BLACK);
copy.symbolizers().add(ls);
}
// check all the geometries that are on line, but not on polygon
geometriesOnLineSymbolizer.removeAll(geometriesOnPolygonSymbolizer);
for (Expression geom : geometriesOnLineSymbolizer) {
Object result = geom.evaluate(schema);
Class geometryType = getTargetGeometryType(result);
if(Polygon.class.isAssignableFrom(geometryType) ||
MultiPolygon.class.isAssignableFrom(geometryType)) {
// we know it's a polygon type, but there is no polygon symbolizer, add one
// in the current rule
copy.symbolizers().add(sb.createPolygonSymbolizer());
} else if(geometryType.equals(Geometry.class)) {
// dynamic, we need to add an extra rule then to paint as polygon
// only if the actual geometry is a polygon type
RuleImpl extra = buildDynamicGeometryRule(copy, geom, sb.createPolygonSymbolizer(),
"Polygon", "MultiPolygon");
extraRules.add(extra);
}
}
// check all the geometries that are on text, but not on any other symbolizer (pure labels)
// that we won't hit otherwise
geometriesOnTextSymbolizer.removeAll(geometriesOnPolygonSymbolizer);
geometriesOnTextSymbolizer.removeAll(geometriesOnLineSymbolizer);
geometriesOnTextSymbolizer.removeAll(geometriesOnPointSymbolizer);
for (Expression geom : geometriesOnTextSymbolizer) {
Object result = geom.evaluate(schema);
Class geometryType = getTargetGeometryType(result);
if (Polygon.class.isAssignableFrom(geometryType)
|| MultiPolygon.class.isAssignableFrom(geometryType)) {
copy.symbolizers().add(sb.createPolygonSymbolizer());
} else if (LineString.class.isAssignableFrom(geometryType)
|| MultiLineString.class.isAssignableFrom(geometryType)) {
copy.symbolizers().add(sb.createLineSymbolizer());
} else if (Point.class.isAssignableFrom(geometryType)
|| MultiPoint.class.isAssignableFrom(geometryType)) {
copy.symbolizers().add(sb.createPointSymbolizer());
} else {
// ouch, it's a generic geometry... now this is going to be painful, we have to
// build a dynamic symbolizer for each possible geometry type
RuleImpl extra = buildDynamicGeometryRule(copy, geom, sb.createPolygonSymbolizer(),
"Polygon", "MultiPolygon");
extraRules.add(extra);
extra = buildDynamicGeometryRule(copy, geom, sb.createLineSymbolizer(),
"LineString", "LinearRing", "MultiLineString");
extraRules.add(extra);
extra = buildDynamicGeometryRule(copy, geom, sb.createPointSymbolizer(), "Point",
"MultiPoint");
extraRules.add(extra);
}
}
}
private RuleImpl buildDynamicGeometryRule(Rule base, Expression geom, Symbolizer symbolizer,
String... geometryTypes) {
List<Filter> typeChecks = new ArrayList<>();
for (String geometryType : geometryTypes) {
typeChecks.add(ff.equal(ff.function("geometryType", geom), ff.literal(geometryType),
false));
}
Filter geomCheck = ff.or(typeChecks);
Filter ruleFilter = base.getFilter();
Filter filter = ruleFilter == null || ruleFilter == Filter.INCLUDE ? geomCheck : ff.and(
geomCheck, ruleFilter);
RuleImpl extra = new RuleImpl(base);
extra.setFilter(filter);
extra.symbolizers().clear();
extra.symbolizers().add(symbolizer);
return extra;
}
private Class getTargetGeometryType(Object descriptor) {
if (!(descriptor instanceof GeometryDescriptor)) {
// we don't know what this will be, we probably evaluated a filter function
return Geometry.class;
} else {
// see if we are dealing with a polygon
return ((GeometryDescriptor) descriptor).getType().getBinding();
}
}
private void addStrokeSymbolizerIfNecessary(Stroke stroke) {
if (stroke != null) {
List<Expression> dashArray = stroke.dashArray();
Graphic graphicStroke = stroke.getGraphicStroke();
if (graphicStroke != null || dashArray != null && dashArray.size() > 0) {
addSolidLineSymbolier = true;
}
}
}
}