/* (c) 2015 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geotools.renderer.lite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.wms.WMSMapContent;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureTypes;
import org.geotools.filter.IllegalFilterException;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.Layer;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.transform.ConcatenatedTransform;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.Rule;
import org.geotools.styling.Style;
import org.geotools.util.logging.Logging;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
/**
* Utility methods to deal with transformations and style based queries.
* <p>
* Note, most code in this class has been taken and adapted from GeoTools' StreamingRenderer.
*/
public class VectorMapRenderUtils {
private static final Logger LOGGER = Logging.getLogger(VectorMapRenderUtils.class);
private static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();
public static Query getStyleQuery(Layer layer, WMSMapContent mapContent) throws IOException {
final ReferencedEnvelope renderingArea = mapContent.getRenderingArea();
final Rectangle screenSize = new Rectangle(mapContent.getMapWidth(),
mapContent.getMapHeight());
final double mapScale;
try {
mapScale = RendererUtilities.calculateScale(renderingArea, mapContent.getMapWidth(),
mapContent.getMapHeight(), null);
} catch (TransformException | FactoryException e) {
throw Throwables.propagate(e);
}
FeatureSource<?, ?> featureSource = layer.getFeatureSource();
FeatureType schema = featureSource.getSchema();
GeometryDescriptor geometryDescriptor = schema.getGeometryDescriptor();
Style style = layer.getStyle();
List<FeatureTypeStyle> featureStyles = style.featureTypeStyles();
List<LiteFeatureTypeStyle> styleList = createLiteFeatureTypeStyles(layer, featureStyles,
schema, mapScale, screenSize);
Query styleQuery;
try {
styleQuery = VectorMapRenderUtils.getStyleQuery(featureSource, styleList, renderingArea,
screenSize, geometryDescriptor);
} catch (IllegalFilterException | FactoryException e1) {
throw Throwables.propagate(e1);
}
Query query = styleQuery;
query.setProperties(Query.ALL_PROPERTIES);
Hints hints = query.getHints();
hints.put(Hints.FEATURE_2D, Boolean.TRUE);
return query;
}
private static Query getStyleQuery(FeatureSource<?, ?> source,
List<LiteFeatureTypeStyle> styleList, ReferencedEnvelope mapArea, Rectangle screenSize,
GeometryDescriptor geometryAttribute)
throws IllegalFilterException, IOException, FactoryException {
final FeatureType schema = source.getSchema();
Query query = new Query(schema.getName().getLocalPart());
query.setProperties(Query.ALL_PROPERTIES);
String geomName = geometryAttribute.getLocalName();
Filter filter = FF.bbox(FF.property(geomName), mapArea);
query.setFilter(filter);
LiteFeatureTypeStyle[] styles = styleList
.toArray(new LiteFeatureTypeStyle[styleList.size()]);
try {
processRuleForQuery(styles, query);
} catch (Exception e) {
throw Throwables.propagate(e);
}
// simplify the filter
SimplifyingFilterVisitor simplifier = new SimplifyingFilterVisitor();
simplifier.setFeatureType(source.getSchema());
Filter simplifiedFilter = (Filter) query.getFilter().accept(simplifier, null);
query.setFilter(simplifiedFilter);
return query;
}
/**
* Builds the transform from sourceCRS to destCRS/
* <p>
* Although we ask for 2D content (via {@link Hints#FEATURE_2D} ) not all DataStore implementations are capable. With that in mind if the provided
* soruceCRS is not 2D we are going to manually post-process the Geomtries into {@link DefaultGeographicCRS#WGS84} - and the
* {@link MathTransform2D} returned here will transition from WGS84 to the requested destCRS.
*
* @param sourceCRS
* @param destCRS
* @return the transform from {@code sourceCRS} to {@code destCRS}, will be an identity transform if the the two crs are equal
* @throws FactoryException If no transform is available to the destCRS
*/
public static MathTransform buildTransform(CoordinateReferenceSystem sourceCRS,
CoordinateReferenceSystem destCRS) throws FactoryException {
Preconditions.checkNotNull(sourceCRS, "sourceCRS");
Preconditions.checkNotNull(destCRS, "destCRS");
MathTransform transform = null;
if (sourceCRS.getCoordinateSystem().getDimension() >= 3) {
// We are going to transform over to DefaultGeographic.WGS84 on the fly
// so we will set up our math transform to take it from there
MathTransform toWgs84_3d = CRS.findMathTransform(sourceCRS,
DefaultGeographicCRS.WGS84_3D);
MathTransform toWgs84_2d = CRS.findMathTransform(DefaultGeographicCRS.WGS84_3D,
DefaultGeographicCRS.WGS84);
transform = ConcatenatedTransform.create(toWgs84_3d, toWgs84_2d);
sourceCRS = DefaultGeographicCRS.WGS84;
}
// the basic crs transformation, if any
MathTransform2D sourceToTarget;
sourceToTarget = (MathTransform2D) CRS.findMathTransform(sourceCRS, destCRS, true);
if (transform == null) {
return sourceToTarget;
}
if (sourceToTarget.isIdentity()) {
return transform;
}
return ConcatenatedTransform.create(transform, sourceToTarget);
}
/**
* JE: If there is a single rule "and" its filter together with the query's filter and send it off to datastore. This will allow as more
* processing to be done on the back end... Very useful if DataStore is a database. Problem is that worst case each filter is ran twice. Next we
* will modify it to find a "Common" filter between all rules and send that to the datastore.
*
* DJB: trying to be smarter. If there are no "elseRules" and no rules w/o a filter, then it makes sense to send them off to the Datastore We
* limit the number of Filters sent off to the datastore, just because it could get a bit rediculous. In general, for a database, if you can limit
* 10% of the rows being returned you're probably doing quite well. The main problem is when your filters really mean you're secretly asking for
* all the data in which case sending the filters to the Datastore actually costs you. But, databases are *much* faster at processing the Filters
* than JAVA is and can use statistical analysis to do it.
*
* @param styles
* @param query
*/
private static void processRuleForQuery(LiteFeatureTypeStyle[] styles, Query query) {
try {
// first we check to see if there are >
// "getMaxFiltersToSendToDatastore" rules
// if so, then we dont do anything since no matter what there's too
// many to send down.
// next we check for any else rules. If we find any --> dont send
// anything to Datastore
// next we check for rules w/o filters. If we find any --> dont send
// anything to Datastore
//
// otherwise, we're gold and can "or" together all the filters then
// AND it with the original filter.
// ie. SELECT * FROM ... WHERE (the_geom && BBOX) AND (filter1 OR
// filter2 OR filter3);
final int maxFilters = 5;
final List<Filter> filtersToDS = new ArrayList<Filter>();
// look at each featuretypestyle
for (LiteFeatureTypeStyle style : styles) {
if (style.elseRules.length > 0) // uh-oh has elseRule
return;
// look at each rule in the featuretypestyle
for (Rule r : style.ruleList) {
if (r.getFilter() == null)
return; // uh-oh has no filter (want all rows)
filtersToDS.add(r.getFilter());
}
}
// if too many bail out
if (filtersToDS.size() > maxFilters)
return;
// or together all the filters
org.opengis.filter.Filter ruleFiltersCombined;
if (filtersToDS.size() == 1) {
ruleFiltersCombined = filtersToDS.get(0);
} else {
ruleFiltersCombined = FF.or(filtersToDS);
}
// combine with the pre-existing filter
ruleFiltersCombined = FF.and(query.getFilter(), ruleFiltersCombined);
query.setFilter(ruleFiltersCombined);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.SEVERE,
"Could not send rules to datastore due to: " + e.getMessage(), e);
}
}
}
private static ArrayList<LiteFeatureTypeStyle> createLiteFeatureTypeStyles(Layer layer,
List<FeatureTypeStyle> featureStyles, FeatureType ftype, double scaleDenominator,
Rectangle screenSize) throws IOException {
ArrayList<LiteFeatureTypeStyle> result = new ArrayList<LiteFeatureTypeStyle>();
LiteFeatureTypeStyle lfts;
for (FeatureTypeStyle fts : featureStyles) {
if (isFeatureTypeStyleActive(ftype, fts)) {
// DJB: this FTS is compatible with this FT.
// get applicable rules at the current scale
List<Rule>[] splittedRules = splitRules(fts, scaleDenominator);
List<Rule> ruleList = splittedRules[0];
List<Rule> elseRuleList = splittedRules[1];
// if none, skip it
if ((ruleList.isEmpty()) && (elseRuleList.isEmpty()))
continue;
// we can optimize this one and draw directly on the graphics, assuming
// there is no composition
Graphics2D graphics = null;
lfts = new LiteFeatureTypeStyle(layer, graphics, ruleList, elseRuleList,
fts.getTransformation());
result.add(lfts);
}
}
return result;
}
private static List<Rule>[] splitRules(final FeatureTypeStyle fts,
final double scaleDenominator) {
List<Rule> ruleList = new ArrayList<Rule>();
List<Rule> elseRuleList = new ArrayList<Rule>();
ruleList = new ArrayList<>();
elseRuleList = new ArrayList<>();
for (Rule r : fts.rules()) {
if (isWithInScale(r, scaleDenominator)) {
if (r.isElseFilter()) {
elseRuleList.add(r);
} else {
ruleList.add(r);
}
}
}
@SuppressWarnings("unchecked")
List<Rule>[] ret = new List[] { ruleList, elseRuleList };
return ret;
}
/**
* Checks if a rule can be triggered at the current scale level
*
* @return true if the scale is compatible with the rule settings
*/
private static boolean isWithInScale(Rule r, double scaleDenominator) {
/** Tolerance used to compare doubles for equality */
final double TOLERANCE = 1e-6;
return ((r.getMinScaleDenominator() - TOLERANCE) <= scaleDenominator)
&& ((r.getMaxScaleDenominator() + TOLERANCE) > scaleDenominator);
}
private static boolean isFeatureTypeStyleActive(FeatureType ftype, FeatureTypeStyle fts) {
// TODO: find a complex feature equivalent for this check
return fts.featureTypeNames().isEmpty() || ((ftype.getName().getLocalPart() != null)
&& (ftype.getName().getLocalPart().equalsIgnoreCase(fts.getFeatureTypeName())
|| FeatureTypes.isDecendedFrom(ftype, null, fts.getFeatureTypeName())));
}
}