/* (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.decorator; import java.awt.Color; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.catalog.WorkspaceInfo; import org.geoserver.kml.KmlEncodingContext; import org.geoserver.kml.icons.IconProperties; import org.geoserver.kml.icons.IconPropertyExtractor; import org.geoserver.kml.icons.IconPropertyInjector; import org.geoserver.wms.WMSInfo; import org.geotools.factory.CommonFactoryFinder; import org.geotools.renderer.style.ExpressionExtractor; import org.geotools.styling.ExternalGraphic; import org.geotools.styling.Fill; import org.geotools.styling.Font; import org.geotools.styling.LineSymbolizer; import org.geotools.styling.PointSymbolizer; import org.geotools.styling.PolygonSymbolizer; import org.geotools.styling.Stroke; import org.geotools.styling.Symbolizer; import org.geotools.styling.TextSymbolizer; import org.geotools.util.logging.Logging; import org.opengis.feature.simple.SimpleFeature; import org.opengis.filter.FilterFactory2; import org.opengis.filter.expression.Expression; import org.opengis.style.GraphicalSymbol; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Polygon; import de.micromata.opengis.kml.v_2_2_0.Feature; import de.micromata.opengis.kml.v_2_2_0.Icon; import de.micromata.opengis.kml.v_2_2_0.IconStyle; import de.micromata.opengis.kml.v_2_2_0.LabelStyle; import de.micromata.opengis.kml.v_2_2_0.LineStyle; import de.micromata.opengis.kml.v_2_2_0.Placemark; import de.micromata.opengis.kml.v_2_2_0.PolyStyle; import de.micromata.opengis.kml.v_2_2_0.Style; /** * Encodes the SLD styles into KML corresponding styles and adds them to the Placemark * * @author Andrea Aime - GeoSolutions */ public class PlacemarkStyleDecoratorFactory implements KmlDecoratorFactory { public KmlDecorator getDecorator(Class<? extends Feature> featureClass, KmlEncodingContext context) { // this decorator makes sense only for WMS if(!(context.getService() instanceof WMSInfo)) { return null; } if (Placemark.class.isAssignableFrom(featureClass)) { return new PlacemarkStyleDecorator(); } else { return null; } } static class PlacemarkStyleDecorator implements KmlDecorator { static final Logger LOGGER = Logging.getLogger(PlacemarkStyleDecorator.class); FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(); @Override public Feature decorate(Feature feature, KmlEncodingContext context) { Placemark pm = (Placemark) feature; // while it's possible to have more than one style object, GE will only paint // the first one Style style = pm.createAndAddStyle(); List<Symbolizer> symbolizers = context.getCurrentSymbolizers(); SimpleFeature sf = context.getCurrentFeature(); if (symbolizers.size() > 0 && sf.getDefaultGeometry() != null) { // sort by point, text, line and polygon Map<Class, List<Symbolizer>> classified = classifySymbolizers(symbolizers); // if no point symbolizers, create a default one List<Symbolizer> points = classified.get(PointSymbolizer.class); if (points.size() == 0) { if(context.isDescriptionEnabled()) { setDefaultIconStyle(style, sf, context); } } else { org.geotools.styling.Style wholeStyle = context.getCurrentLayer().getStyle(); IconProperties properties = IconPropertyExtractor.extractProperties(wholeStyle, sf); setIconStyle(style, wholeStyle, properties, context); } // handle label styles List<Symbolizer> texts = classified.get(TextSymbolizer.class); if (texts.size() == 0) { if(context.isDescriptionEnabled()) { setDefaultLabelStyle(style); } } else { // the XML schema allows only one text style, follow painter's model // and set the last one TextSymbolizer lastTextSymbolizer = (TextSymbolizer) texts.get(texts.size() - 1); setLabelStyle(style, sf, lastTextSymbolizer); } // handle line styles List<Symbolizer> lines = classified.get(LineSymbolizer.class); // the XML schema allows only one line style, follow painter's model // and set the last one if(lines.size() > 0) { LineSymbolizer lastLineSymbolizer = (LineSymbolizer) lines.get(lines.size() - 1); setLineStyle(style, sf, lastLineSymbolizer.getStroke()); } // handle polygon styles boolean forceOutiline = lines.size() == 0; List<Symbolizer> polygons = classified.get(PolygonSymbolizer.class); if(polygons.size() > 0) { // the XML schema allows only one polygon style, follow painter's model // and set the last one PolygonSymbolizer lastPolygonSymbolizer = (PolygonSymbolizer) polygons.get(polygons.size() - 1); setPolygonStyle(style, sf, lastPolygonSymbolizer, forceOutiline); } } return feature; } private Map<Class, List<Symbolizer>> classifySymbolizers(List<Symbolizer> symbolizers) { Map<Class, List<Symbolizer>> result = new HashMap<Class, List<Symbolizer>>(); result.put(PointSymbolizer.class, new ArrayList<Symbolizer>()); result.put(LineSymbolizer.class, new ArrayList<Symbolizer>()); result.put(PolygonSymbolizer.class, new ArrayList<Symbolizer>()); result.put(TextSymbolizer.class, new ArrayList<Symbolizer>()); for (Symbolizer s : symbolizers) { if (s instanceof PointSymbolizer) { result.get(PointSymbolizer.class).add(s); } else if (s instanceof LineSymbolizer) { result.get(LineSymbolizer.class).add(s); } else if (s instanceof PolygonSymbolizer) { result.get(PolygonSymbolizer.class).add(s); } else if (s instanceof TextSymbolizer) { result.get(TextSymbolizer.class).add(s); } else { throw new IllegalArgumentException("Unrecognized symbolizer type: " + s); } } return result; } protected void setDefaultIconStyle(Style style, SimpleFeature feature, KmlEncodingContext context) { // figure out if line or polygon boolean line = feature.getDefaultGeometry() != null && (feature.getDefaultGeometry() instanceof LineString || feature .getDefaultGeometry() instanceof MultiLineString); boolean poly = feature.getDefaultGeometry() != null && (feature.getDefaultGeometry() instanceof Polygon || feature .getDefaultGeometry() instanceof MultiPolygon); // Final pre-flight check if (!line && !poly) { LOGGER.log(Level.FINER, "Unexpectedly entered encodeDefaultIconStyle() " + "with something that does not have a multipoint geometry."); return; } IconStyle is = style.createAndSetIconStyle(); // make transparent if they ask for attributes, since we'll have a label if (context.isDescriptionEnabled()) { is.setColor("00ffffff"); } // if line or polygon scale the label if (line || poly) { is.setScale(0.4); } String imageURL = "http://icons.opengeo.org/markers/icon-" + (poly ? "poly.1" : "line.1") + ".png"; Icon icon = is.createAndSetIcon(); icon.setHref(imageURL); icon.setViewBoundScale(1); } /** * Encodes a KML IconStyle from a point style and symbolizer. */ protected void setIconStyle(Style style, org.geotools.styling.Style sld, IconProperties properties, KmlEncodingContext context) { if (context.isLiveIcons() || properties.isExternal()) { setLiveIconStyle(style, sld, properties, context); } else { setInlineIconStyle(style, sld, properties, context); } } protected void setInlineIconStyle(Style style, org.geotools.styling.Style sld, IconProperties properties, KmlEncodingContext context) { final String name = properties.getIconName(sld); Map<String,org.geotools.styling.Style> iconStyles = context.getIconStyles(); if (!iconStyles.containsKey(name)) { final org.geotools.styling.Style injectedStyle = IconPropertyInjector.injectProperties(sld, properties.getProperties()); iconStyles.put(name, injectedStyle); } final Double scale = properties.getScale(); final String path = "icons/" + name + ".png"; IconStyle is = style.createAndSetIconStyle(); if (properties.getHeading() != null) { is.setHeading(0.0); } if (scale != null) { is.setScale(scale); } Icon icon = is.createAndSetIcon(); icon.setHref(path); } protected void setLiveIconStyle(Style style, org.geotools.styling.Style sld, IconProperties properties, KmlEncodingContext context) { final Double opacity = properties.getOpacity(); final Double scale = properties.getScale(); final Double heading = properties.getHeading(); IconStyle is = style.createAndSetIconStyle(); if (opacity != null) { is.setColor(colorToHex(Color.WHITE, opacity)); } if (scale != null) { is.setScale(scale); } if (heading != null) { is.setHeading(heading); } // Get the name of the workspace WorkspaceInfo ws = context.getWms().getCatalog().getStyleByName(sld.getName()).getWorkspace(); String wsName = null; if(ws!=null) wsName = ws.getName(); Icon icon = is.createAndSetIcon(); icon.setHref(properties.href(context.getMapContent().getRequest().getBaseUrl(), wsName, sld.getName())); } /** * Encodes a transparent KML LabelStyle */ protected void setDefaultLabelStyle(Style style) { LabelStyle ls = style.createAndSetLabelStyle(); ls.setColor("00ffffff"); } protected void setLabelStyle(Style style, SimpleFeature feature, TextSymbolizer symbolizer) { LabelStyle ls = style.createAndSetLabelStyle(); double scale = 1; Font font = symbolizer.getFont(); if(font != null && font.getSize() != null) { // we make the scale proportional to the normal font size double size = font.getSize().evaluate(feature, Double.class); scale = Math.round(size / Font.DEFAULT_FONTSIZE * 100) / 100.0; } ls.setScale(scale); Fill fill = symbolizer.getFill(); if (fill != null) { Double opacity = fill.getOpacity().evaluate(feature, Double.class); if (opacity == null || Double.isNaN(opacity)) { opacity = 1.0; } Color color = fill.getColor().evaluate(feature, Color.class); ls.setColor(colorToHex(color, opacity)); } else { ls.setColor("ffffffff"); } } /** * Encodes a KML IconStyle + PolyStyle from a polygon style and symbolizer. */ protected void setPolygonStyle(Style style, SimpleFeature feature, PolygonSymbolizer symbolizer, boolean forceOutline) { // if stroke specified add line style as well (it has to be before the fill, otherwise // we'll get a white filling...) if (symbolizer.getStroke() != null) { setLineStyle(style, feature, symbolizer.getStroke()); } // fill PolyStyle ps = style.createAndSetPolyStyle(); Fill fill = symbolizer.getFill(); if (fill != null) { // get opacity Double opacity = fill.getOpacity().evaluate(feature, Double.class); if (opacity == null || Double.isNaN(opacity)) { opacity = 1.0; } Color color = (Color) fill.getColor().evaluate(feature, Color.class); ps.setColor(colorToHex(color, opacity)); } else { // make it transparent ps.setColor("00aaaaaa"); } // outline if (symbolizer.getStroke() != null || forceOutline) { ps.setOutline(true); } } /** * Encodes a KML IconStyle + LineStyle from a polygon style and symbolizer. */ protected void setLineStyle(Style style, SimpleFeature feature, Stroke stroke) { LineStyle ls = style.createAndSetLineStyle(); if (stroke != null) { // opacity Double opacity = stroke.getOpacity().evaluate(feature, Double.class); if (opacity == null || Double.isNaN(opacity)) { opacity = 1.0; } Color color = null; Expression sc = stroke.getColor(); if (sc != null) { color = (Color) sc.evaluate(feature, Color.class); } if (color == null) { color = Color.DARK_GRAY; } ls.setColor(colorToHex(color, opacity)); // width Double width = null; Expression sw = stroke.getWidth(); if (sw != null) { width = sw.evaluate(feature, Double.class); } if (width == null) { width = 1d; } ls.setWidth(width); } else { // default ls.setColor("ffaaaaaa"); ls.setWidth(1); } } private ExternalGraphic getExternalGraphic(PointSymbolizer symbolizer) { for (GraphicalSymbol s : symbolizer.getGraphic().graphicalSymbols()) { if (s instanceof ExternalGraphic) { return (ExternalGraphic) s; } } return null; } /** * Does value substitution on a URL with embedded CQL expressions * * @param strLocation the URL as a string, possibly with expressions * @param feature the feature providing the context in which the expressions are evaluated * @return a string containing the final URL */ protected String evaluateDynamicSymbolizer(String strLocation, SimpleFeature feature) { if (strLocation == null) return null; // parse the eventual ${cqlExpression} embedded in the URL Expression location; try { location = ExpressionExtractor.extractCqlExpressions(strLocation); } catch (IllegalArgumentException e) { // in the unlikely event that a URL is using one of the chars reserved for // ${cqlExpression} // let's try and use the location as a literal if (LOGGER.isLoggable(Level.SEVERE)) LOGGER.log(Level.SEVERE, "Could not parse cql expressions out of " + strLocation, e); location = ff.literal(strLocation); } return location.evaluate(feature, String.class); } /** * 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". */ 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(); } /** * 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 */ String intToHex(int i) { String prelim = Integer.toHexString(i); if (prelim.length() < 2) { prelim = "0" + prelim; } return prelim; } } }