/* (c) 2014 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.kml.utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.catalog.Catalog; import org.geoserver.kml.KmlEncodingContext; import org.geoserver.kml.regionate.RegionatingStrategy; import org.geoserver.kml.regionate.RegionatingStrategyFactory; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; import org.geoserver.wms.WMS; import org.geoserver.wms.WMSMapContent; import org.geotools.data.DataUtilities; import org.geotools.data.Query; import org.geotools.data.crs.ReprojectFeatureResults; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.factory.CommonFactoryFinder; 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.styling.Style; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Envelope; /** * Class encapsulating the logic to query features for a given layer in the current GetMap KML * request * * @author Andrea Aime - GeoSolutions * @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org */ public class KMLFeatureAccessor { static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geoserver.kml"); /** * Factory used to create filter objects */ private static FilterFactory filterFactory = (FilterFactory) CommonFactoryFinder .getFilterFactory(null); /** * Loads the feature collection based on the current styling and the scale denominator. If no * feature is going to be returned a null feature collection will be returned instead * * @param layer * @param mapContent * @param wms * @param scaleDenominator * */ public SimpleFeatureCollection loadFeatureCollection(Layer layer, WMSMapContent mapContent, WMS wms, double scaleDenominator) throws Exception { SimpleFeatureSource featureSource = (SimpleFeatureSource) layer.getFeatureSource(); Query q = getFeaturesQuery(layer, mapContent, wms, scaleDenominator); // make sure we output in 4326 since that's what KML mandates CoordinateReferenceSystem wgs84; try { wgs84 = CRS.decode("EPSG:4326", true); } catch (Exception e) { throw new RuntimeException( "Cannot decode EPSG:4326, the CRS subsystem must be badly broken...", e); } SimpleFeatureCollection features = featureSource.getFeatures(q); SimpleFeatureType schema = featureSource.getSchema(); CoordinateReferenceSystem nativeCRS = schema.getCoordinateReferenceSystem(); if (nativeCRS != null && !CRS.equalsIgnoreMetadata(wgs84, nativeCRS)) { features = new ReprojectFeatureResults(features, wgs84); } return features; } /** * Counts how many features will be returned for the specified layer in the current request * * @param layer * @param mapContent * @param wms * @param scaleDenominator * */ public int getFeatureCount(Layer layer, WMSMapContent mapContent, WMS wms, double scaleDenominator) throws Exception { Query q = getFeaturesQuery(layer, mapContent, wms, scaleDenominator); SimpleFeatureSource featureSource = (SimpleFeatureSource) layer.getFeatureSource(); int count = featureSource.getCount(q); if (count == -1) { count = featureSource.getFeatures(q).size(); } return count; } /** * Builds the Query object that will return the features for the specified layer and scale * denominator, based also on the current WMS configuration * * @param layer * @param mapContent * @param wms * @param scaleDenominator * @param schema * * @throws TransformException * @throws FactoryException */ private Query getFeaturesQuery(Layer layer, WMSMapContent mapContent, WMS wms, double scaleDenominator) throws TransformException, FactoryException { SimpleFeatureType schema = ((SimpleFeatureSource) layer.getFeatureSource()).getSchema(); // create a bbox filter in the source crs (from the GetMap request bbox) ReferencedEnvelope aoi = mapContent.getRenderingArea(); Filter filter = createBBoxFilter(schema, aoi); // now build the query using only the attributes and the bounding box needed Query q = new Query(schema.getTypeName()); q.setFilter(filter); // now, if a definition query has been established for this layer, // be sure to respect it by combining it with the bounding box one. q = DataUtilities.mixQueries(q, layer.getQuery(), "KMLEncoder"); // check the regionating strategy RegionatingStrategy regionatingStrategy = null; String stratname = (String) mapContent.getRequest().getFormatOptions().get("regionateBy"); if (("auto").equals(stratname)) { Catalog catalog = wms.getGeoServer().getCatalog(); Name name = layer.getFeatureSource().getName(); stratname = catalog.getFeatureTypeByName(name).getMetadata() .get("kml.regionateStrategy", String.class); if (stratname == null || "".equals(stratname)) { stratname = "best_guess"; LOGGER.log(Level.FINE, "No default regionating strategy has been configured in " + name + "; using automatic best-guess strategy."); } } Filter regionatingFilter = Filter.INCLUDE; if (stratname != null) { regionatingStrategy = findStrategyByName(stratname); // if a strategy was specified but we did not find it, let the user // know if (regionatingStrategy == null) { throw new ServiceException("Unknown regionating strategy " + stratname); } else { regionatingFilter = regionatingStrategy.getFilter(mapContent, layer); } } // try to load less features by leveraging regionating strategy and the SLD Filter ruleFilter = getStyleFilter(schema, layer.getStyle(), scaleDenominator); Filter finalFilter = joinFilters(q.getFilter(), ruleFilter, regionatingFilter); if (finalFilter == Filter.EXCLUDE) { // if we don't have any feature to return return null; } q.setFilter(finalFilter); return q; } private RegionatingStrategy findStrategyByName(String name) { List<RegionatingStrategyFactory> factories = GeoServerExtensions .extensions(RegionatingStrategyFactory.class); Iterator<RegionatingStrategyFactory> it = factories.iterator(); while (it.hasNext()) { RegionatingStrategyFactory factory = it.next(); if (factory.canHandle(name)) { return factory.createStrategy(); } } return null; } private Filter getStyleFilter(SimpleFeatureType schema, Style style, double scaleDenominator) { // first, simplify the style and get only the applicable rules ScaleStyleVisitor sdSimplifier = new ScaleStyleVisitor(scaleDenominator, schema); style.accept(sdSimplifier); Style simplified = sdSimplifier.getSimplifiedStyle(); // then collect the filter equivalent to all the rules RuleFiltersCollector collector = new RuleFiltersCollector(); simplified.accept(collector); return collector.getSummaryFilter(); } /** * Creates the bounding box filters (one for each geometric attribute) needed to query a * <code>Layer</code>'s feature source to return just the features for the target rendering * extent * * @param schema the layer's feature source schema * @param bbox the expression holding the target rendering bounding box * @throws IllegalFilterException if something goes wrong creating the filter */ private Filter createBBoxFilter(SimpleFeatureType schema, ReferencedEnvelope aoi) throws IllegalFilterException { // Google earth likes to make requests that go beyond 180 when zoomed out // fix them List<ReferencedEnvelope> envelopes = new ArrayList<ReferencedEnvelope>(); if(KmlEncodingContext.WORLD_BOUNDS_WGS84.contains((Envelope) aoi)) { envelopes.add(aoi); } else { Envelope intersection = KmlEncodingContext.WORLD_BOUNDS_WGS84.intersection((Envelope) aoi); if(intersection.getWidth() > 0) { envelopes.add(new ReferencedEnvelope(intersection, DefaultGeographicCRS.WGS84)); } // look for the portion beyond +180 if(aoi.getMaxX() > 180) { // GE never sends values larger than 360 double maxx = aoi.getMaxX() - 360; double minx = aoi.getMinX() > 180 ? aoi.getMinX() - 360 : -180; envelopes.add(new ReferencedEnvelope(minx, maxx, aoi.getMinY(), aoi.getMaxY(), DefaultGeographicCRS.WGS84)); } } List<ReferencedEnvelope> sourceEnvelopes = new ArrayList<ReferencedEnvelope>(); CoordinateReferenceSystem sourceCrs = schema.getCoordinateReferenceSystem(); if ((sourceCrs != null) && !CRS.equalsIgnoreMetadata(aoi.getCoordinateReferenceSystem(), sourceCrs)) { for (ReferencedEnvelope re : envelopes) { try { ReferencedEnvelope se = re.transform(sourceCrs, true); sourceEnvelopes.add(se); } catch(Exception e) { // in case of failure it means we are going beyond the projectable area // of the source system, meaning that we are asking for an area that's too // large -> don't do spatial filtering then return Filter.INCLUDE; } } } else { sourceEnvelopes.addAll(envelopes); } GeometryDescriptor gd = schema.getGeometryDescriptor(); if(sourceEnvelopes.size() == 0) { return Filter.INCLUDE; } else if(sourceEnvelopes.size() == 1) { ReferencedEnvelope se = sourceEnvelopes.get(0); return filterFactory.bbox(gd.getLocalName(), se.getMinX(), se.getMinY(), se.getMaxX(), se.getMaxY(), null); } else { // we have to OR the multiple source envelopes List<Filter> filters = new ArrayList<Filter>(); for (ReferencedEnvelope se : sourceEnvelopes) { filters.add(filterFactory.bbox(gd.getLocalName(), se.getMinX(), se.getMinY(), se.getMaxX(), se.getMaxY(), null)); } return filterFactory.or(filters); } } /** * Joins the provided filters in a single one by and-ing them (and then, simplifying them) * * @param filters * */ private Filter joinFilters(Filter... filters) { if (filters == null || filters.length == 0) { return Filter.EXCLUDE; } Filter result = null; if (filters.length > 0) { FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); result = ff.and(Arrays.asList(filters)); } else if (filters.length == 1) { result = filters[0]; } SimplifyingFilterVisitor visitor = new SimplifyingFilterVisitor(); return (Filter) result.accept(visitor, null); } }