/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wms.legendgraphic; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import org.geoserver.catalog.LegendInfo; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; import org.geoserver.wms.GetLegendGraphicRequest; import org.geoserver.wms.GetLegendGraphicRequest.LegendRequest; import org.geoserver.wms.map.ImageUtils; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.data.DataUtilities; import org.geotools.data.Parameter; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.SchemaException; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.feature.type.GeometryDescriptorImpl; import org.geotools.feature.type.GeometryTypeImpl; import org.geotools.geometry.jts.LiteShape2; import org.geotools.process.Processors; import org.geotools.process.function.ProcessFunction; import org.geotools.renderer.lite.RendererUtilities; import org.geotools.renderer.lite.StyledShapePainter; import org.geotools.renderer.style.SLDStyleFactory; import org.geotools.renderer.style.Style2D; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.LineSymbolizer; import org.geotools.styling.PointSymbolizer; import org.geotools.styling.PolygonSymbolizer; import org.geotools.styling.RasterSymbolizer; import org.geotools.styling.Rule; import org.geotools.styling.Style; import org.geotools.styling.Symbolizer; import org.geotools.styling.TextSymbolizer; import org.geotools.styling.visitor.DpiRescaleStyleVisitor; import org.geotools.styling.visitor.DuplicatingStyleVisitor; import org.geotools.styling.visitor.UomRescaleStyleVisitor; import org.geotools.util.NumberRange; import org.opengis.feature.Feature; import org.opengis.feature.IllegalAttributeException; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.FeatureType; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.GeometryType; import org.opengis.feature.type.Name; import org.opengis.filter.FilterFactory; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.style.GraphicLegend; import org.springframework.util.StringUtils; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * Template {@linkPlain org.vfny.geoserver.responses.wms.GetLegendGraphicProducer} based on * <a href="http://svn.geotools.org/geotools/trunk/gt/module/main/src/org/geotools/renderer/lite/StyledShapePainter.java"> GeoTools StyledShapePainter</a> * that produces a BufferedImage with the appropriate legend graphic for a given GetLegendGraphic WMS request. * * <p> * It should be enough for a subclass to implement {@linkPlain * org.vfny.geoserver.responses.wms.GetLegendGraphicProducer#writeTo(OutputStream)} and * <code>getContentType()</code> in order to encode the BufferedImage produced by this class to the * appropriate output format. * </p> * * <p> * This class takes literally the fact that the arguments <code>WIDTH</code> and <code>HEIGHT</code> * are just <i>hints</i> about the desired dimensions of the produced graphic, and the need to * produce a legend graphic representative enough of the SLD style for which it is being generated. * Thus, if no <code>RULE</code> parameter was passed and the style has more than one applicable * Rule for the actual scale factor, there will be generated a legend graphic of the specified * width, but with as many stacked graphics as applicable rules were found, providing by this way a * representative enough legend. * </p> * * @author Gabriel Roldan * @author Simone Giannecchini, GeoSolutions SAS * @version $Id$ */ public class BufferedImageLegendGraphicBuilder { Logger LOGGER = Logger.getLogger("org.geoserver.wms.legendgraphic"); /** Tolerance used to compare doubles for equality */ public static final double TOLERANCE = 1e-6; /** * Singleton shape painter to serve all legend requests. We can use a single shape painter * instance as long as it remains thread safe. */ private static final StyledShapePainter shapePainter = new StyledShapePainter(); /** * used to create sample point shapes with LiteShape (not lines nor polygons) */ private static final GeometryFactory geomFac = new GeometryFactory(); /** * Just a holder to avoid creating many polygon shapes from inside <code>getSampleShape()</code> */ private LiteShape2 sampleRect; /** * Just a holder to avoid creating many line shapes from inside <code>getSampleShape()</code> */ private LiteShape2 sampleLine; /** * Just a holder to avoid creating many point shapes from inside <code>getSampleShape()</code> */ private LiteShape2 samplePoint; /** * Default minimum size for symbols rendering. * Can be overridden using LEGEND_OPTIONS (minSymbolSize). */ private final double MINIMUM_SYMBOL_SIZE = 3.0; /** * Default constructor. Subclasses may provide its own with a String parameter to establish its * desired output format, if they support more than one (e.g. a JAI based one) */ public BufferedImageLegendGraphicBuilder() { super(); } /** * Takes a GetLegendGraphicRequest and produces a BufferedImage that then can be used by a * subclass to encode it to the appropriate output format. * * @param request * the "parsed" request, where "parsed" means that it's values are already validated * so this method must not take care of verifying the requested layer exists and the * like. * * * @throws ServiceException * if there are problems creating a "sample" feature instance for the FeatureType * <code>request</code> returns as the required layer (which should not occur). */ public BufferedImage buildLegendGraphic(GetLegendGraphicRequest request) throws ServiceException { // list of images to be rendered for the layers (more than one if // a layer group is given) List<RenderedImage> layersImages=new ArrayList<RenderedImage>(); List<LegendRequest> layers = request.getLegends(); boolean forceLabelsOn = false; boolean forceLabelsOff = false; if (request.getLegendOptions().get("forceLabels") instanceof String) { String forceLabelsOpt = (String) request.getLegendOptions().get("forceLabels"); if (forceLabelsOpt.equalsIgnoreCase("on")) { forceLabelsOn = true; } else if (forceLabelsOpt.equalsIgnoreCase("off")) { forceLabelsOff = true; } } boolean forceTitlesOff = false; if (request.getLegendOptions().get("forceTitles") instanceof String) { String forceTitlesOpt = (String) request.getLegendOptions().get("forceTitles"); if (forceTitlesOpt.equalsIgnoreCase("off")) { forceTitlesOff = true; } } FeatureCountProcessor countProcessor = null; if(Boolean.TRUE.equals(request.getLegendOption(GetLegendGraphicRequest.COUNT_MATCHED_KEY, Boolean.class))) { countProcessor = new FeatureCountProcessor(request); } for(LegendRequest legend : layers ){ FeatureType layer=legend.getFeatureType(); // style and rule to use for the current layer Style gt2Style = legend.getStyle(); if (gt2Style == null) { throw new NullPointerException("request.getStyle()"); } // get rule corresponding to the layer index // normalize to null for NO RULE String ruleName = legend.getRule(); // was null // width and height, we might have to rescale those in case of DPI usage int w = request.getWidth(); int h = request.getHeight(); // apply dpi rescale double dpi = RendererUtilities.getDpi(request.getLegendOptions()); double standardDpi = RendererUtilities.getDpi(Collections.emptyMap()); if(dpi != standardDpi) { double scaleFactor = dpi / standardDpi; w = (int) Math.round(w * scaleFactor); h = (int) Math.round(h * scaleFactor); DpiRescaleStyleVisitor dpiVisitor = new DpiRescaleStyleVisitor(scaleFactor); dpiVisitor.visit(gt2Style); gt2Style = (Style) dpiVisitor.getCopy(); } // apply UOM rescaling if we have a scale if (request.getScale() > 0) { double pixelsPerMeters = RendererUtilities.calculatePixelsPerMeterRatio(request.getScale(), request.getLegendOptions()); UomRescaleStyleVisitor rescaleVisitor = new UomRescaleStyleVisitor(pixelsPerMeters); rescaleVisitor.visit(gt2Style); gt2Style = (Style) rescaleVisitor.getCopy(); } boolean strict = request.isStrict(); final boolean transparent = request.isTransparent(); RenderedImage titleImage=null; // if we have more than one layer, we put a title on top of each layer legend if(layers.size() > 1 && !forceTitlesOff) { titleImage=getLayerTitle(legend, w, h, transparent, request); } // Check for rendering transformation boolean hasVectorTransformation = false; boolean hasRasterTransformation = false; List<FeatureTypeStyle> ftsList = gt2Style.featureTypeStyles(); for (int i=0; i<ftsList.size(); i++) { FeatureTypeStyle fts = ftsList.get(i); Expression exp = fts.getTransformation(); if (exp != null) { ProcessFunction processFunction = (ProcessFunction) exp; Name processName = processFunction.getProcessName(); Map<String, Parameter<?>> outputs = Processors.getResultInfo(processName, null); if (outputs.isEmpty()) { continue; } Parameter<?> output = outputs.values().iterator().next(); // we assume there is only one output if (SimpleFeatureCollection.class.isAssignableFrom(output.getType())) { hasVectorTransformation = true; break; } else if (GridCoverage2D.class.isAssignableFrom(output.getType())) { hasRasterTransformation = true; break; } } } final boolean buildRasterLegend = (!strict && layer == null && LegendUtils.checkRasterSymbolizer(gt2Style)) || (LegendUtils.checkGridLayer(layer) && !hasVectorTransformation) || hasRasterTransformation; // Just checks LegendInfo currently, should check gtStyle final boolean useProvidedLegend = layer != null && legend.getLayerInfo() != null; RenderedImage legendImage = null; if (useProvidedLegend) { legendImage = getLayerLegend(legend, w, h, transparent, request); } if (buildRasterLegend) { final RasterLayerLegendHelper rasterLegendHelper = new RasterLayerLegendHelper(request,gt2Style,ruleName); final BufferedImage image = rasterLegendHelper.getLegend(); if(image != null) { if(titleImage != null) { layersImages.add(titleImage); } layersImages.add(image); } } else if (useProvidedLegend && legendImage!=null) { if (titleImage != null) { layersImages.add(titleImage); } layersImages.add(legendImage); } else { final Feature sampleFeature; if (layer == null || hasVectorTransformation) { sampleFeature = createSampleFeature(); } else { sampleFeature = createSampleFeature(layer); } final FeatureTypeStyle[] ftStyles = gt2Style.featureTypeStyles().toArray( new FeatureTypeStyle[0]); final double scaleDenominator = request.getScale(); Rule[] applicableRules; if (ruleName != null) { Rule rule = LegendUtils.getRule(ftStyles, ruleName); if (rule == null) { throw new ServiceException("Specified style does not contains a rule named " + ruleName); } applicableRules = new Rule[] {rule}; } else { applicableRules = LegendUtils.getApplicableRules(ftStyles, scaleDenominator); } // do we have to alter the style to do context sensitive feature counts? if(countProcessor != null && !forceLabelsOff) { applicableRules = updateRuleTitles(countProcessor, legend, applicableRules); } final NumberRange<Double> scaleRange = NumberRange.create(scaleDenominator, scaleDenominator); final int ruleCount = applicableRules.length; /** * A legend graphic is produced for each applicable rule. They're being held * here until the process is done and then painted on a "stack" like legend. */ final List<RenderedImage> legendsStack = new ArrayList<RenderedImage>(ruleCount); final SLDStyleFactory styleFactory = new SLDStyleFactory(); double minimumSymbolSize = MINIMUM_SYMBOL_SIZE; // get minSymbolSize from LEGEND_OPTIONS, if defined if (request.getLegendOptions().get("minSymbolSize") instanceof String) { String minSymbolSizeOpt = (String) request.getLegendOptions() .get("minSymbolSize"); try { minimumSymbolSize = Double.parseDouble(minSymbolSizeOpt); } catch (NumberFormatException e) { throw new IllegalArgumentException( "Invalid minSymbolSize value: should be a number"); } } // calculate the symbols rescaling factor necessary for them to be // drawn inside the icon box double symbolScale = calcSymbolScale(w, h, layer, sampleFeature, applicableRules, minimumSymbolSize); for (int i = 0; i < ruleCount; i++) { final RenderedImage image = ImageUtils.createImage(w, h, (IndexColorModel) null, transparent); final Map<RenderingHints.Key, Object> hintsMap = new HashMap<RenderingHints.Key, Object>(); final Graphics2D graphics = ImageUtils.prepareTransparency(transparent, LegendUtils.getBackgroundColor(request), image, hintsMap); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Feature sample = getSampleFeatureForRule(layer, sampleFeature, applicableRules[i]); FilterFactory ff = CommonFactoryFinder.getFilterFactory(); final Symbolizer[] symbolizers = applicableRules[i].getSymbolizers(); final GraphicLegend graphic = applicableRules[i].getLegend(); // If this rule has a legend graphic defined in the SLD, use it if (graphic != null) { if (this.samplePoint == null) { Coordinate coord = new Coordinate(w / 2, h / 2); try { this.samplePoint = new LiteShape2(geomFac.createPoint(coord), null, null, false); } catch (Exception e) { this.samplePoint = null; } } shapePainter.paint(graphics, this.samplePoint, graphic, scaleDenominator, false); } else { for (int sIdx = 0; sIdx < symbolizers.length; sIdx++) { Symbolizer symbolizer = symbolizers[sIdx]; if (symbolizer instanceof RasterSymbolizer) { // skip it } else { // rescale symbols if needed if (symbolScale > 1.0 && symbolizer instanceof PointSymbolizer) { PointSymbolizer pointSymbolizer = cloneSymbolizer(symbolizer); if (pointSymbolizer.getGraphic() != null) { double size = getPointSymbolizerSize(sample, pointSymbolizer, Math.min(w, h) - 4); pointSymbolizer.getGraphic().setSize( ff.literal(size / symbolScale + minimumSymbolSize)); symbolizer = pointSymbolizer; } } Style2D style2d = styleFactory.createStyle(sample, symbolizer, scaleRange); LiteShape2 shape = getSampleShape(symbolizer, w, h); if (style2d != null) { shapePainter.paint(graphics, shape, style2d, scaleDenominator); } } } } if(image != null && titleImage != null) { layersImages.add(titleImage); titleImage = null; } legendsStack.add(image); graphics.dispose(); } int labelMargin = 3; if(!StringUtils.isEmpty(request.getLegendOptions().get("labelMargin"))) { labelMargin = Integer.parseInt(request.getLegendOptions().get("labelMargin").toString()); } LegendMerger.MergeOptions options = LegendMerger.MergeOptions.createFromRequest(legendsStack, 0, 0, 0, labelMargin, request, forceLabelsOn, forceLabelsOff); BufferedImage image = LegendMerger.mergeLegends(applicableRules, request, options); if(image != null) { layersImages.add(image); } } } // all legend graphics are merged if we have a layer group BufferedImage finalLegend = mergeGroups(layersImages,null,request, forceLabelsOn, forceLabelsOff); if(finalLegend == null) { throw new IllegalArgumentException("no legend passed"); } return finalLegend; } protected Rule[] updateRuleTitles(FeatureCountProcessor processor, LegendRequest legend, Rule[] applicableRules) { return processor.preProcessRules(legend, applicableRules); } /** * Clones the given (Point)Symbolizer. * * @param symbolizer symbolizer to clone * @return cloned PointSymbolizer */ private PointSymbolizer cloneSymbolizer(Symbolizer symbolizer) { DuplicatingStyleVisitor duplicator = new DuplicatingStyleVisitor(); symbolizer.accept(duplicator); PointSymbolizer pointSymbolizer = (PointSymbolizer) duplicator .getCopy(); return pointSymbolizer; } /** * Calculates a global rescaling factor for all the symbols * to be drawn in the given rules. This is to be sure all symbols * are drawn inside the given w x h box. * * @param width horizontal constraint * @param height vertical constraint * @param featureType FeatureType to be used for size extraction in expressions * (used to create a sample if feature is null) * @param feature Feature to be used for size extraction in expressions * (if null a sample Feature will be created from featureType) * @param rules set of rules to scan for symbols * @param minimumSymbolSize lower constraint for the symbols size * */ private double calcSymbolScale(int width, int height, FeatureType featureType, Feature feature, final Rule[] rules, double minimumSymbolsSize) { // check for max and min size in rendered symbols double minSize = Double.MAX_VALUE; double maxSize = 0.0; final int ruleCount = rules.length; for (int i = 0; i < ruleCount; i++) { Feature sample = getSampleFeatureForRule(featureType, feature, rules[i]); final Symbolizer[] symbolizers = rules[i].getSymbolizers(); for (int sIdx = 0; sIdx < symbolizers.length; sIdx++) { final Symbolizer symbolizer = symbolizers[sIdx]; if (symbolizer instanceof PointSymbolizer) { double size = getPointSymbolizerSize(sample, (PointSymbolizer) symbolizer, Math.min(width, height)); if (size < minSize) { minSize = size; } if (size > maxSize) { maxSize = size; } } } } if(minSize != maxSize) { return (maxSize - minSize + 1) / (Math.min(width, height) - minimumSymbolsSize); } else { return maxSize / (Math.min(width, height) - minimumSymbolsSize); } } /** * Gets a numeric value for the given PointSymbolizer * * @param feature sample to be used for evals * @param pointSymbolizer symbolizer * @param defaultSize size to use is none can be taken from the symbolizer */ private double getPointSymbolizerSize(Feature feature, PointSymbolizer pointSymbolizer, int defaultSize) { if (pointSymbolizer.getGraphic() != null) { Expression sizeExp = pointSymbolizer.getGraphic().getSize(); if (sizeExp instanceof Literal) { Object size = sizeExp.evaluate(feature); if (size != null) { if (size instanceof Double) { return (Double) size; } try { return Double.parseDouble(size.toString()); } catch (NumberFormatException e) { return defaultSize; } } } } return defaultSize; } /** * Returns a sample feature for the given rule, with the following criteria: - * if a sample is given in input is returned in output - if a sample is not * given in input, scan the rule symbolizers to find the one with the max * dimensionality, and return a sample for that dimensionality. * * @param featureType featureType used to create a sample, if none is given as * input * @param sample feature sample to be returned as is in output, if defined * @param rule rule containing symbolizers to scan for max dimensionality * */ private Feature getSampleFeatureForRule(FeatureType featureType, Feature sample, final Rule rule) { Symbolizer[] symbolizers = rule.getSymbolizers(); // if we don't have a sample as input, we need to create a sampleFeature // looking at the requested symbolizers (we chose the one with the max // dimensionality and create a congruent sample) if (sample == null) { int dimensionality = 1; for (int sIdx = 0; sIdx < symbolizers.length; sIdx++) { final Symbolizer symbolizer = symbolizers[sIdx]; if (LineSymbolizer.class.isAssignableFrom(symbolizer.getClass())) { dimensionality = 2; } if (PolygonSymbolizer.class.isAssignableFrom(symbolizer.getClass())) { dimensionality = 3; } } return createSampleFeature(featureType, dimensionality); } else { return sample; } } /** * Renders a title for a layer (to be put on top of the layer legend). * * @param legend FeatureType representing the layer * @param w width for the image (hint) * @param h height for the image (hint) * @param transparent (should the image be transparent) * @param request GetLegendGraphicRequest being built * @return image with the title */ private RenderedImage getLayerTitle(LegendRequest legend, int w, int h, boolean transparent, GetLegendGraphicRequest request) { String title=legend.getTitle(); final BufferedImage image = ImageUtils.createImage(w, h, (IndexColorModel) null, transparent); return LegendMerger.getRenderedLabel(image,title, request); } /** * Extracts legend for layer based on LayerInfo configuration or style LegendGraphics. * * @param published FeatureType representing the layer * @param w width for the image (hint) * @param h height for the image (hint) * @param transparent (should the image be transparent) * @param request GetLegendGraphicRequest being built * @return image with the title */ private RenderedImage getLayerLegend(LegendRequest legend, int w, int h, boolean transparent, GetLegendGraphicRequest request) { LegendInfo legendInfo = legend.getLegendInfo(); if (legendInfo == null) { return null; // nothing provided will need to dynamically generate } String onlineResource = legendInfo.getOnlineResource(); if( onlineResource == null || onlineResource.isEmpty() ){ return null; // nothing provided will need to dynamically generate } URL url = null; try { url = new URL( onlineResource ); } catch(MalformedURLException invalid){ LOGGER.fine( "Unable to obtain "+onlineResource ); return null; // should log this! } try { BufferedImage image = ImageIO.read(url); if( image.getWidth() == w && image.getHeight() == h ){ return image; } final BufferedImage rescale = ImageUtils.createImage(w, h, (IndexColorModel) null,true); Graphics2D g = (Graphics2D) rescale.getGraphics(); g.setColor(new Color(255,255,255,0)); g.fillRect(0, 0, w, h); double aspect = ((double)h)/((double)image.getHeight()); int legendWidth = (int)(aspect*((double)image.getWidth())); g.drawImage(image, 0, 0, legendWidth, h, null); g.dispose(); return rescale; } catch(IOException notFound){ LOGGER.log(Level.FINE, "Unable to legend graphic:"+url, notFound ); return null; // unable to access image } } /** * Receives a list of <code>BufferedImages</code> and produces a new one * which holds all the images in <code>imageStack</code> one above the * other, handling labels. * * @param imageStack * the list of BufferedImages, one for each applicable Rule * @param rules * The applicable rules, one for each image in the stack (if not * null it's used to compute labels) * @param request * The request. * @param forceLabelsOn * true for force labels on also with a single image. * @param forceLabelsOff * true for force labels off also with more than one rule. * * @return the stack image with all the images on the argument list. * * @throws IllegalArgumentException * if the list is empty */ private BufferedImage mergeGroups(List<RenderedImage> imageStack, Rule[] rules, GetLegendGraphicRequest req, boolean forceLabelsOn, boolean forceLabelsOff) { LegendMerger.MergeOptions options = LegendMerger.MergeOptions.createFromRequest(imageStack, 0, 0, 0, 0, req, forceLabelsOn, forceLabelsOff); options.setLayout(LegendUtils.getGroupLayout(req)); return LegendMerger.mergeGroups(rules, options); } /** * Returns a <code>java.awt.Shape</code> appropiate to render a legend graphic given the * symbolizer type and the legend dimensions. * * @param symbolizer * the Symbolizer for whose type a sample shape will be created * @param legendWidth * the requested width, in output units, of the legend graphic * @param legendHeight * the requested height, in output units, of the legend graphic * * @return an appropiate Line2D, Rectangle2D or LiteShape(Point) for the symbolizer, wether it * is a LineSymbolizer, a PolygonSymbolizer, or a Point ot Text Symbolizer * * @throws IllegalArgumentException * if an unknown symbolizer impl was passed in. */ private LiteShape2 getSampleShape(Symbolizer symbolizer, int legendWidth, int legendHeight) { LiteShape2 sampleShape; final float hpad = (legendWidth * LegendUtils.hpaddingFactor); final float vpad = (legendHeight * LegendUtils.vpaddingFactor); if (symbolizer instanceof LineSymbolizer) { if (this.sampleLine == null) { Coordinate[] coords = { new Coordinate(hpad, legendHeight - vpad - 1), new Coordinate(legendWidth - hpad - 1, vpad) }; LineString geom = geomFac.createLineString(coords); try { this.sampleLine = new LiteShape2(geom, null, null, false); } catch (Exception e) { this.sampleLine = null; } } sampleShape = this.sampleLine; } else if ((symbolizer instanceof PolygonSymbolizer) || (symbolizer instanceof RasterSymbolizer)) { if (this.sampleRect == null) { final float w = legendWidth - (2 * hpad) - 1; final float h = legendHeight - (2 * vpad) - 1; Coordinate[] coords = { new Coordinate(hpad, vpad), new Coordinate(hpad, vpad + h), new Coordinate(hpad + w, vpad + h), new Coordinate(hpad + w, vpad), new Coordinate(hpad, vpad) }; LinearRing shell = geomFac.createLinearRing(coords); Polygon geom = geomFac.createPolygon(shell, null); try { this.sampleRect = new LiteShape2(geom, null, null, false); } catch (Exception e) { this.sampleRect = null; } } sampleShape = this.sampleRect; } else if (symbolizer instanceof PointSymbolizer || symbolizer instanceof TextSymbolizer) { if (this.samplePoint == null) { Coordinate coord = new Coordinate(legendWidth / 2, legendHeight / 2); try { this.samplePoint = new LiteShape2(geomFac.createPoint(coord), null, null, false); } catch (Exception e) { this.samplePoint = null; } } sampleShape = this.samplePoint; } else { throw new IllegalArgumentException("Unknown symbolizer: " + symbolizer); } return sampleShape; } private SimpleFeature createSampleFeature() { SimpleFeatureType type; try { type = DataUtilities.createType("Sample", "the_geom:Geometry"); } catch (SchemaException e) { throw new RuntimeException(e); } return SimpleFeatureBuilder.template((SimpleFeatureType) type, null); } /** * Creates a sample Feature instance in the hope that it can be used in the * rendering of the legend graphic, using the given dimensionality for the * geometry attribute. * * @param schema the schema for which to create a sample Feature instance * @param dimensionality the geometry dimensionality required (ovverides the one * defined in the schema) 1= points, 2= lines, 3= polygons * * @throws ServiceException */ private Feature createSampleFeature(FeatureType schema, int dimensionality) throws ServiceException { if (schema instanceof SimpleFeatureType) { schema = cloneWithDimensionality(schema, dimensionality); } return createSampleFeature(schema); } /** * Clones the given schema, changing the geometry attribute to match the given * dimensionality. * * @param schema schema to clone * @param dimensionality dimensionality for the geometry 1= points, 2= lines, 3= * polygons * */ private FeatureType cloneWithDimensionality(FeatureType schema, int dimensionality) { SimpleFeatureType simpleFt = (SimpleFeatureType) schema; SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); builder.setName(schema.getName()); builder.setCRS(schema.getCoordinateReferenceSystem()); for (AttributeDescriptor desc : simpleFt.getAttributeDescriptors()) { if (isMixedGeometry(desc)) { GeometryDescriptor geomDescriptor = (GeometryDescriptor) desc; GeometryType geomType = geomDescriptor.getType(); Class<?> geometryClass = getGeometryForDimensionality(dimensionality); GeometryType gt = new GeometryTypeImpl(geomType.getName(), geometryClass, geomType.getCoordinateReferenceSystem(), geomType.isIdentified(), geomType.isAbstract(), geomType.getRestrictions(), geomType.getSuper(), geomType.getDescription()); builder.add(new GeometryDescriptorImpl(gt, geomDescriptor.getName(), geomDescriptor.getMinOccurs(), geomDescriptor.getMaxOccurs(), geomDescriptor.isNillable(), geomDescriptor.getDefaultValue())); } else { builder.add(desc); } } schema = builder.buildFeatureType(); return schema; } /** * Creates a Geometry class for the given dimensionality. * * @param dimensionality * */ private Class<?> getGeometryForDimensionality(int dimensionality) { if (dimensionality == 1) { return Point.class; } if (dimensionality == 2) { return LineString.class; } return Polygon.class; } /** * Creates a sample Feature instance in the hope that it can be used in the rendering of the * legend graphic. * * @param schema * the schema for which to create a sample Feature instance * * * * @throws ServiceException */ private Feature createSampleFeature(FeatureType schema) throws ServiceException { Feature sampleFeature; try { if (schema instanceof SimpleFeatureType) { if (hasMixedGeometry((SimpleFeatureType)schema)) { // we can't create a sample for a generic Geometry type sampleFeature = null; } else { sampleFeature = SimpleFeatureBuilder.template((SimpleFeatureType) schema, null); } } else { sampleFeature = DataUtilities.templateFeature(schema); } } catch (IllegalAttributeException e) { throw new ServiceException(e); } return sampleFeature; } /** * Checks if the given schema contains a GeometryDescriptor that has a generic * Geometry type. * * @param schema * */ private boolean hasMixedGeometry(SimpleFeatureType schema) { for (AttributeDescriptor attDesc : schema.getAttributeDescriptors()) { if(isMixedGeometry(attDesc)) { return true; } } return false; } /** * Checks if the given AttributeDescriptor describes a generic Geometry. * * @param attDesc */ private boolean isMixedGeometry(AttributeDescriptor attDesc) { if (attDesc instanceof GeometryDescriptor && attDesc.getType().getBinding() == Geometry.class) { return true; } return false; } }