/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved. * This code is licensed under the GPL 2.0 license, availible at the root * application directory. */ package org.vfny.geoserver.wms.responses.map.kml; import java.awt.Color; import java.util.ArrayList; 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.platform.GeoServerExtensions; import org.geoserver.wms.util.WMSRequests; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultQuery; import org.geotools.data.FeatureSource; import org.geotools.data.Query; import org.geotools.data.crs.ReprojectFeatureResults; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureTypes; import org.geotools.filter.IllegalFilterException; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.MapLayer; import org.geotools.referencing.CRS; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.Rule; import org.geotools.styling.Style; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; 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.crs.CoordinateReferenceSystem; import org.vfny.geoserver.wms.WMSMapContext; import org.vfny.geoserver.wms.WmsException; import org.xml.sax.Attributes; import org.xml.sax.helpers.AttributesImpl; import com.vividsolutions.jts.geom.Envelope; /** * Some convenience methods used by the kml transformers. * * @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org * @deprecated use {@link WMSRequests}. */ public class KMLUtils { /** * logger */ static Logger LOGGER = org.geotools.util.logging.Logging .getLogger("org.geoserver.kml"); /** * Tolerance used to compare doubles for equality */ static final double TOLERANCE = 1e-6; private static final CoordinateReferenceSystem WGS84; private static final int RULES = 0; private static final int ELSE_RULES = 1; static { try { WGS84 = CRS.decode("EPSG:4326"); } catch (Exception e) { throw new RuntimeException( "Cannot decode EPSG:4326, the CRS subsystem must be badly broken..."); } } public final static Envelope WORLD_BOUNDS_WGS84 = new Envelope(-180,180,-90,90); private final static double[] TILE_RESOLUTIONS = new double[100]; static { for (int i = 0; i < TILE_RESOLUTIONS.length; i++) { TILE_RESOLUTIONS[i] = WORLD_BOUNDS_WGS84.getWidth() / ((0x01 << i) * 256); } } /** * Factory used to create filter objects */ private static FilterFactory filterFactory = (FilterFactory) CommonFactoryFinder .getFilterFactory(null); /** * Encodes the url of a GetMap request from a map context + map layer. * <p> * If the <tt>mapLayer</tt> argument is <code>null</code>, the request is * made including all layers in the <tt>mapContexT</tt>. * </p> * <p> * If the <tt>bbox</tt> argument is <code>null</code>. {@link WMSMapContext#getAreaOfInterest()} * is used for the bbox parameter. * </p> * * @param mapContext The map context. * @param mapLayer The Map layer, may be <code>null</code>. * @param layerIndex The index of the layer in the request. * @param bbox The bounding box of the request, may be <code>null</code>. * @param kvp Additional or overiding kvp parameters, may be <code>null</code> * @param tile Flag controlling whether the request should be made against tile cache * * @return The full url for a getMap request. * @deprecated use {@link WMSRequests#getGetMapUrl(WMSMapContext, MapLayer, Envelope, String[])} */ public static String getMapUrl( WMSMapContext mapContext, MapLayer mapLayer, int layerIndex, Envelope bbox, String[] kvp, boolean tile) { if ( tile ) { return WMSRequests.getTiledGetMapUrl( mapContext.getRequest(), mapLayer, layerIndex, bbox, kvp ); } return WMSRequests.getGetMapUrl( mapContext.getRequest(), mapLayer, layerIndex, bbox, kvp ); } /** * Encodes the url of a GetMap request from a map context + map layer. * <p> * If the <tt>mapLayer</tt> argument is <code>null</code>, the request is * made including all layers in the <tt>mapContexT</tt>. * </p> * @param mapContext The map context. * @param mapLayer The Map layer, may be <code>null</code> * @param layerIndex The index of the layer in the request. * @param kvp Additional or overidding kvp parameters, may be <code>null</code> * @param tile Flag controlling wether the request should be made against tile cache * * @return The full url for a getMap request. * @deprecated use {@link WMSRequests#getGetMapUrl(WMSMapContext, MapLayer, int, Envelope, String[])} */ public static String getMapUrl(WMSMapContext mapContext, MapLayer mapLayer, int layerIndex, boolean tile) { return getMapUrl(mapContext, mapLayer, layerIndex, mapContext.getAreaOfInterest(), null, tile); } /** * Encodes the url for a GetLegendGraphic request from a map context + map layer. * * @param mapContext The map context. * @param mapLayer The map layer. * @param kvp Additional or overidding kvp parameters, may be <code>null</code> * * @return A map containing all the key value pairs for a GetLegendGraphic request. * @deprecated use {@link WMSRequests#getGetLegendGraphicUrl(WMSMapContext, MapLayer, String[]) */ public static String getLegendGraphicUrl(WMSMapContext mapContext, MapLayer mapLayer, String[] kvp) { return WMSRequests.getGetLegendGraphicUrl(mapContext.getRequest(), mapLayer, kvp); } /** * Creates sax attributes from an array of key value pairs. * * @param nameValuePairs Alternating key value pair array. * */ public static Attributes attributes(String[] nameValuePairs) { AttributesImpl attributes = new AttributesImpl(); for (int i = 0; i < nameValuePairs.length; i += 2) { String name = nameValuePairs[i]; String value = nameValuePairs[i + 1]; attributes.addAttribute("", name, name, "", value); } return attributes; } /** * Filters the rules of <code>featureTypeStyle</code> returnting only * those that apply to <code>feature</code>. * <p> * This method returns rules for which: * <ol> * <li><code>rule.getFilter()</code> matches <code>feature</code>, or: * <li>the rule defines an "ElseFilter", and the feature matches no * other rules. * </ol> * This method returns an empty array in the case of which no rules * match. * </p> * @param featureTypeStyle The feature type style containing the rules. * @param feature The feature being filtered against. * */ public static Rule[] filterRules(FeatureTypeStyle featureTypeStyle, SimpleFeature feature, double scaleDenominator) { Rule[] rules = featureTypeStyle.getRules(); if ((rules == null) || (rules.length == 0)) { return new Rule[0]; } ArrayList filtered = new ArrayList(rules.length); //process the rules, keep track of the need to apply an else filters boolean match = false; boolean hasElseFilter = false; for (int i = 0; i < rules.length; i++) { Rule rule = rules[i]; LOGGER.finer(new StringBuffer("Applying rule: ").append( rule.toString()).toString()); //does this rule have an else filter if (rule.hasElseFilter()) { hasElseFilter = true; continue; } //is this rule within scale? if (!isWithInScale(rule, scaleDenominator)) { continue; } //does this rule have a filter which applies to the feature Filter filter = rule.getFilter(); if ((filter == null) || filter.evaluate(feature)) { match = true; filtered.add(rule); } } //if no rules mached the feautre, re-run through the rules applying // any else filters if (!match && hasElseFilter) { //loop through again and apply all the else rules for (int i = 0; i < rules.length; i++) { Rule rule = rules[i]; //is this rule within scale? if (!isWithInScale(rule, scaleDenominator)) { continue; } if (rule.hasElseFilter()) { filtered.add(rule); } } } return (Rule[]) filtered.toArray(new Rule[filtered.size()]); } /** * Checks if a rule can be triggered at the current scale level * * @param r * The rule * @return true if the scale is compatible with the rule settings */ public static boolean isWithInScale(Rule r, double scaleDenominator) { return ((r.getMinScaleDenominator() - TOLERANCE) <= scaleDenominator) && ((r.getMaxScaleDenominator() + TOLERANCE) > scaleDenominator); } public static int findZoomLevel(Envelope extent){ double resolution = Math.max(extent.getWidth()/256d, extent.getHeight() / 256d); int i; for (i = 1; i < TILE_RESOLUTIONS.length; i++){ if (resolution > TILE_RESOLUTIONS[i]) { i--; break; } } return i; } public static Envelope expandToTile(Envelope extent){ double resolution = Math.max(extent.getWidth() / 256d, extent.getHeight() / 256d); int i = findZoomLevel(extent); while (i > 0) { resolution = TILE_RESOLUTIONS[i]; double tilelon = resolution * 256; double tilelat = resolution * 256; double lon0 = extent.getMinX() - WORLD_BOUNDS_WGS84.getMinX(); double lon1 = extent.getMaxX() - WORLD_BOUNDS_WGS84.getMinX(); int col0 = (int) Math.floor(lon0 / tilelon); int col1 = (int) Math.floor((lon1 / tilelon) - 1E-9); double lat0 = extent.getMinY() - WORLD_BOUNDS_WGS84.getMinY(); double lat1 = extent.getMaxY() - WORLD_BOUNDS_WGS84.getMinY(); int row0 = (int) Math.floor(lat0 / tilelat); int row1 = (int) Math.floor((lat1 / tilelat) - 1E-9); if ((col0 == col1) && (row0 == row1)) { double tileoffsetlon = WORLD_BOUNDS_WGS84.getMinX() + (col0 * tilelon); double tileoffsetlat = WORLD_BOUNDS_WGS84.getMinY() + (row0 * tilelat); return new Envelope(tileoffsetlon, tileoffsetlon + tilelon, tileoffsetlat, tileoffsetlat + tilelat); } else { i--; } } return WORLD_BOUNDS_WGS84; } /** * Utility method to convert an int into hex, padded to two characters. * handy for generating colour strings. * * @param i Int to convert * @return String a two character hex representation of i * NOTE: this is a utility method and should be put somewhere more useful. */ public static String intToHex(int i) { String prelim = Integer.toHexString(i); if (prelim.length() < 2) { prelim = "0" + prelim; } return prelim; } /** * Utility method to convert a Color and opacity (0,1.0) into a KML * color ref. * * @param c The color to convert. * @param opacity Opacity / alpha, double from 0 to 1.0. * * @return A String of the form "AABBGGRR". */ public static String colorToHex(Color c, double opacity) { return new StringBuffer().append( intToHex(new Float(255 * opacity).intValue())).append( intToHex(c.getBlue())).append(intToHex(c.getGreen())).append( intToHex(c.getRed())).toString(); } /** * Filters the feature type styles of <code>style</code> returning only * those that apply to <code>featureType</code> * <p> * This methods returns feature types for which * <code>featureTypeStyle.getFeatureTypeName()</code> matches the name * of the feature type of <code>featureType</code>, or matches the name of * any parent type of the feature type of <code>featureType</code>. This * method returns an empty array in the case of which no rules match. * </p> * @param style The style containing the feature type styles. * @param featureType The feature type being filtered against. * */ public static FeatureTypeStyle[] filterFeatureTypeStyles(Style style, SimpleFeatureType featureType) { FeatureTypeStyle[] featureTypeStyles = style.getFeatureTypeStyles(); if ((featureTypeStyles == null) || (featureTypeStyles.length == 0)) { return new FeatureTypeStyle[0]; } ArrayList filtered = new ArrayList(featureTypeStyles.length); for (int i = 0; i < featureTypeStyles.length; i++) { FeatureTypeStyle featureTypeStyle = featureTypeStyles[i]; String featureTypeName = featureTypeStyle.getFeatureTypeName(); //does this style have any rules if (featureTypeStyle.getRules() == null || featureTypeStyle.getRules().length == 0) { continue; } //does this style apply to the feature collection if (featureType.getTypeName().equalsIgnoreCase(featureTypeName) || FeatureTypes.isDecendedFrom(featureType, null, featureTypeName)) { filtered.add(featureTypeStyle); } } return (FeatureTypeStyle[]) filtered .toArray(new FeatureTypeStyle[filtered.size()]); } public static FeatureCollection<SimpleFeatureType, SimpleFeature> loadFeatureCollection( FeatureSource<SimpleFeatureType, SimpleFeature> featureSource, MapLayer layer, WMSMapContext mapContext) throws Exception { SimpleFeatureType schema = featureSource.getSchema(); Envelope envelope = mapContext.getAreaOfInterest(); ReferencedEnvelope aoi = new ReferencedEnvelope(envelope, mapContext .getCoordinateReferenceSystem()); CoordinateReferenceSystem sourceCrs = schema .getCoordinateReferenceSystem(); boolean reprojectBBox = (sourceCrs != null) && !CRS.equalsIgnoreMetadata( aoi.getCoordinateReferenceSystem(), sourceCrs); if (reprojectBBox) { aoi = aoi.transform(sourceCrs, true); } Filter filter = createBBoxFilter(schema, aoi); // now build the query using only the attributes and the bounding // box needed DefaultQuery q = new DefaultQuery(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. Query definitionQuery = layer.getQuery(); if (definitionQuery != Query.ALL) { if (q == Query.ALL) { q = (DefaultQuery) definitionQuery; } else { q = (DefaultQuery) DataUtilities.mixQueries(definitionQuery, q, "KMLEncoder"); } } // handle startIndex requested by client query q.setStartIndex(definitionQuery.getStartIndex()); // check the regionating strategy RegionatingStrategy regionatingStrategy = null; String stratname = (String) mapContext.getRequest().getFormatOptions() .get("regionateBy"); if (("auto").equals(stratname)) { Catalog catalog = mapContext.getRequest().getWMS().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." ); } } 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 WmsException("Unknown regionating strategy " + stratname); } // try to load less features by leveraging regionating strategy and the // SLD Filter regionatingFilter = Filter.INCLUDE; if (regionatingStrategy != null) regionatingFilter = regionatingStrategy.getFilter(mapContext, layer); Filter ruleFilter = summarizeRuleFilters(getLayerRules(featureSource .getSchema(), layer.getStyle())); Filter finalFilter = joinFilters(q.getFilter(), joinFilters(ruleFilter, regionatingFilter)); q.setFilter(finalFilter); // make sure we output in 4326 since that's what KML mandates if (sourceCrs != null && !CRS.equalsIgnoreMetadata(WGS84, sourceCrs)) { return new ReprojectFeatureResults(featureSource.getFeatures(q), WGS84); } return featureSource.getFeatures(q); } public static 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; } /** * Creates the bounding box filters (one for each geometric attribute) * needed to query a <code>MapLayer</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 * @return an or'ed list of bbox filters, one for each geometric attribute * in <code>attributes</code>. If there are just one geometric * attribute, just returns its corresponding * <code>GeometryFilter</code>. * @throws IllegalFilterException * if something goes wrong creating the filter */ private static Filter createBBoxFilter(SimpleFeatureType schema, Envelope bbox) throws IllegalFilterException { List filters = new ArrayList(); for (int j = 0; j < schema.getAttributeCount(); j++) { AttributeDescriptor attType = schema.getDescriptor(j); if (attType instanceof GeometryDescriptor) { Filter gfilter = filterFactory.bbox(attType.getLocalName(), bbox.getMinX(), bbox.getMinY(), bbox.getMaxX(), bbox .getMaxY(), null); filters.add(gfilter); } } if (filters.size() == 0) return Filter.INCLUDE; else if (filters.size() == 1) return (Filter) filters.get(0); else return filterFactory.or(filters); } private static List[] getLayerRules(SimpleFeatureType ftype, Style style) { List[] result = new List[] { new ArrayList(), new ArrayList() }; final String typeName = ftype.getTypeName(); FeatureTypeStyle[] featureStyles = style.getFeatureTypeStyles(); final int length = featureStyles.length; for (int i = 0; i < length; i++) { // getting feature styles FeatureTypeStyle fts = featureStyles[i]; // check if this FTS is compatible with this FT. if ((typeName != null) && FeatureTypes.isDecendedFrom(ftype, null, fts .getFeatureTypeName())) { // get applicable rules at the current scale Rule[] ftsRules = fts.getRules(); for (int j = 0; j < ftsRules.length; j++) { // getting rule Rule r = ftsRules[j]; if (KMLUtils.isWithInScale(r, 1)) { if (r.hasElseFilter()) { result[ELSE_RULES].add(r); } else { result[RULES].add(r); } } } } } return result; } private static Filter joinFilters(Filter first, Filter second) { if (Filter.EXCLUDE.equals(first) || Filter.EXCLUDE.equals(second)) return Filter.EXCLUDE; if (first == null || Filter.INCLUDE.equals(first)) return second; if (second == null || Filter.INCLUDE.equals(second)) return first; FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); return ff.and(first, second); } /** * Summarizes, when possible, the rule filters into one. * * @param rules * @param originalFiter * @return */ private static Filter summarizeRuleFilters(List[] rules) { if (rules[RULES].size() == 0 || rules[ELSE_RULES].size() > 0) return Filter.INCLUDE; List filters = new ArrayList(); for (Iterator it = rules[RULES].iterator(); it.hasNext();) { Rule rule = (Rule) it.next(); // if there is a single rule asking for all filters, we have to // return everything that the original filter returned already if (rule.getFilter() == null || Filter.INCLUDE.equals(rule.getFilter())) return Filter.INCLUDE; else filters.add(rule.getFilter()); } FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); return ff.or(filters); } }