001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
002: * This code is licensed under the GPL 2.0 license, availible at the root
003: * application directory.
004: */
005: package org.vfny.geoserver.wms.responses;
006:
007: import java.awt.Canvas;
008: import java.awt.Color;
009: import java.awt.Font;
010: import java.awt.Graphics2D;
011: import java.awt.RenderingHints;
012: import java.awt.geom.Rectangle2D;
013: import java.awt.image.BufferedImage;
014: import java.awt.image.ImageObserver;
015: import java.awt.image.IndexColorModel;
016: import java.awt.image.RenderedImage;
017: import java.io.File;
018: import java.io.IOException;
019: import java.net.MalformedURLException;
020: import java.net.URL;
021: import java.util.ArrayList;
022: import java.util.HashMap;
023: import java.util.List;
024: import java.util.Map;
025: import java.util.StringTokenizer;
026: import java.util.logging.Level;
027: import java.util.logging.Logger;
028:
029: import javax.imageio.ImageIO;
030:
031: import org.geotools.feature.AttributeType;
032: import org.geotools.feature.Feature;
033: import org.geotools.feature.FeatureType;
034: import org.geotools.feature.IllegalAttributeException;
035: import org.geotools.geometry.jts.LiteShape2;
036: import org.geotools.renderer.lite.StyledShapePainter;
037: import org.geotools.renderer.style.SLDStyleFactory;
038: import org.geotools.renderer.style.Style2D;
039: import org.geotools.styling.FeatureTypeStyle;
040: import org.geotools.styling.LineSymbolizer;
041: import org.geotools.styling.PointSymbolizer;
042: import org.geotools.styling.PolygonSymbolizer;
043: import org.geotools.styling.RasterSymbolizer;
044: import org.geotools.styling.Rule;
045: import org.geotools.styling.Style;
046: import org.geotools.styling.Symbolizer;
047: import org.geotools.styling.TextSymbolizer;
048: import org.geotools.util.NumberRange;
049: import org.vfny.geoserver.global.GeoserverDataDirectory;
050: import org.vfny.geoserver.wms.GetLegendGraphicProducer;
051: import org.vfny.geoserver.wms.WmsException;
052: import org.vfny.geoserver.wms.requests.GetLegendGraphicRequest;
053:
054: import com.vividsolutions.jts.geom.Coordinate;
055: import com.vividsolutions.jts.geom.GeometryFactory;
056: import com.vividsolutions.jts.geom.LineString;
057: import com.vividsolutions.jts.geom.LinearRing;
058: import com.vividsolutions.jts.geom.Polygon;
059:
060: /**
061: * Template {@linkPlain
062: * org.vfny.geoserver.responses.wms.GetLegendGraphicProducer} based on
063: * GeoTools' {@link
064: * http://svn.geotools.org/geotools/trunk/gt/module/main/src/org/geotools/renderer/lite/StyledShapePainter.java
065: * StyledShapePainter} that produces a BufferedImage with the appropiate
066: * legend graphic for a given GetLegendGraphic WMS request.
067: *
068: * <p>
069: * It should be enough for a subclass to implement {@linkPlain
070: * org.vfny.geoserver.responses.wms.GetLegendGraphicProducer#writeTo(OutputStream)}
071: * and <code>getContentType()</code> in order to encode the BufferedImage
072: * produced by this class to the appropiate output format.
073: * </p>
074: *
075: * <p>
076: * This class takes literally the fact that the arguments <code>WIDTH</code>
077: * and <code>HEIGHT</code> are just <i>hints</i> about the desired dimensions
078: * of the produced graphic, and the need to produce a legend graphic
079: * representative enough of the SLD style for which it is being generated.
080: * Thus, if no <code>RULE</code> parameter was passed and the style has more
081: * than one applicable Rule for the actual scale factor, there will be
082: * generated a legend graphic of the specified width, but with as many stacked
083: * graphics as applicable rules were found, providing by this way a
084: * representative enough legend.
085: * </p>
086: *
087: * @author Gabriel Roldan, Axios Engineering
088: * @version $Id: DefaultRasterLegendProducer.java 7945 2007-12-05 18:09:38Z aaime $
089: */
090: public abstract class DefaultRasterLegendProducer implements
091: GetLegendGraphicProducer {
092: /** shared package's logger */
093: private static final Logger LOGGER = org.geotools.util.logging.Logging
094: .getLogger(DefaultRasterLegendProducer.class.getPackage()
095: .getName());
096:
097: /** Factory that will resolve symbolizers into rendered styles */
098: private static final SLDStyleFactory styleFactory = new SLDStyleFactory();
099:
100: /** Tolerance used to compare doubles for equality */
101: private static final double TOLERANCE = 1e-6;
102:
103: /**
104: * Singleton shape painter to serve all legend requests. We can use a
105: * single shape painter instance as long as it remains thread safe.
106: */
107: private static final StyledShapePainter shapePainter = new StyledShapePainter(
108: null);
109:
110: /**
111: * used to create sample point shapes with LiteShape (not lines nor
112: * polygons)
113: */
114: private static final GeometryFactory geomFac = new GeometryFactory();
115:
116: /**
117: * Default Legend graphics background color
118: */
119: public static final Color BG_COLOR = Color.WHITE;
120: /**
121: * Default label color
122: */
123: public static final Color FONT_COLOR = Color.BLACK;
124: /**
125: * Image observer to help in creating the stack like legend graphic from
126: * the images created for each rule
127: */
128: private static final ImageObserver imgObs = new Canvas();
129:
130: /** padding percentaje factor at both sides of the legend. */
131: private static final float hpaddingFactor = 0.15f;
132:
133: /** top & bottom padding percentaje factor for the legend */
134: private static final float vpaddingFactor = 0.15f;
135:
136: /** The image produced at <code>produceLegendGraphic</code> */
137: private BufferedImage legendGraphic;
138:
139: /**
140: * set to <code>true</code> when <code>abort()</code> gets called,
141: * indicates that the rendering of the legend graphic should stop
142: * gracefully as soon as possible
143: */
144: private boolean renderingStopRequested;
145:
146: /**
147: * Just a holder to avoid creating many polygon shapes from inside
148: * <code>getSampleShape()</code>
149: */
150: private LiteShape2 sampleRect;
151:
152: /**
153: * Just a holder to avoid creating many line shapes from inside
154: * <code>getSampleShape()</code>
155: */
156: private LiteShape2 sampleLine;
157:
158: /**
159: * Just a holder to avoid creating many point shapes from inside
160: * <code>getSampleShape()</code>
161: */
162: private LiteShape2 samplePoint;
163:
164: /**
165: * Default constructor. Subclasses may provide its own with a String
166: * parameter to establish its desired output format, if they support more
167: * than one (e.g. a JAI based one)
168: */
169: public DefaultRasterLegendProducer() {
170: super ();
171: }
172:
173: /**
174: * Takes a GetLegendGraphicRequest and produces a BufferedImage that then
175: * can be used by a subclass to encode it to the appropiate output format.
176: *
177: * @param request the "parsed" request, where "parsed" means that it's
178: * values are already validated so this method must not take care
179: * of verifying the requested layer exists and the like.
180: *
181: * @throws WmsException if there are problems creating a "sample" feature
182: * instance for the FeatureType <code>request</code> returns as
183: * the required layer (which should not occur).
184: */
185: public void produceLegendGraphic(GetLegendGraphicRequest request)
186: throws WmsException {
187: final Feature sampleFeature = createSampleFeature(request
188: .getLayer());
189:
190: final Style gt2Style = request.getStyle();
191: final FeatureTypeStyle[] ftStyles = gt2Style
192: .getFeatureTypeStyles();
193:
194: final double scaleDenominator = request.getScale();
195:
196: final Rule[] applicableRules;
197:
198: if (request.getRule() != null) {
199: applicableRules = new Rule[] { request.getRule() };
200: } else {
201: applicableRules = getApplicableRules(ftStyles,
202: scaleDenominator);
203: }
204:
205: final NumberRange scaleRange = new NumberRange(
206: scaleDenominator, scaleDenominator);
207:
208: final int ruleCount = applicableRules.length;
209:
210: /**
211: * A legend graphic is produced for each applicable rule. They're being
212: * held here until the process is done and then painted on a "stack"
213: * like legend.
214: */
215: final List /*<BufferedImage>*/legendsStack = new ArrayList(
216: ruleCount);
217:
218: final int w = request.getWidth();
219: final int h = request.getHeight();
220:
221: final Color bgColor = getBackgroundColor(request);
222: for (int i = 0; i < ruleCount; i++) {
223: Symbolizer[] symbolizers = applicableRules[i]
224: .getSymbolizers();
225:
226: //BufferedImage image = prepareImage(w, h, request.isTransparent());
227: final boolean transparent = request.isTransparent();
228: final RenderedImage image = ImageUtils.createImage(w, h,
229: (IndexColorModel) null, transparent);
230: final Map hintsMap = new HashMap();
231: Graphics2D graphics = ImageUtils.prepareTransparency(
232: transparent, bgColor, image, hintsMap);
233: graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
234: RenderingHints.VALUE_ANTIALIAS_ON);
235:
236: for (int sIdx = 0; sIdx < symbolizers.length; sIdx++) {
237: Symbolizer symbolizer = symbolizers[sIdx];
238:
239: if (symbolizer instanceof RasterSymbolizer) {
240: BufferedImage imgShape;
241:
242: try {
243: File styles = GeoserverDataDirectory
244: .findCreateConfigDir("styles");
245: File rasterLegend = new File(styles,
246: "rasterLegend.png");
247: if (rasterLegend.exists())
248: imgShape = ImageIO.read(rasterLegend);
249: else
250: imgShape = ImageIO
251: .read(DefaultRasterLegendProducer.class
252: .getResource("rasterLegend.png"));
253: } catch (Exception e) {
254: LOGGER.log(Level.SEVERE, e
255: .getLocalizedMessage(), e);
256: throw new WmsException(e);
257: }
258:
259: graphics.drawImage(imgShape, 0, 0, w, h, null);
260: } else {
261: Style2D style2d = styleFactory.createStyle(
262: sampleFeature, symbolizer, scaleRange);
263: LiteShape2 shape = getSampleShape(symbolizer, w, h);
264:
265: if (style2d != null) {
266: shapePainter.paint(graphics, shape, style2d,
267: scaleDenominator);
268: }
269: }
270: }
271:
272: legendsStack.add(image);
273: }
274:
275: //JD: changd legend behaviour, see GEOS-812
276: //this.legendGraphic = scaleImage(mergeLegends(legendsStack), request);
277: this .legendGraphic = mergeLegends(legendsStack,
278: applicableRules, request);
279: }
280:
281: /**
282: * Recieves a list of <code>BufferedImages</code> and produces a new one
283: * which holds all the images in <code>imageStack</code> one above the
284: * other.
285: *
286: * @param imageStack the list of BufferedImages, one for each applicable
287: * Rule
288: * @param rules The applicable rules, one for each image in the stack
289: * @param request The request.
290: *
291: * @return the stack image with all the images on the argument list.
292: *
293: * @throws IllegalArgumentException if the list is empty
294: */
295: private static BufferedImage mergeLegends(List imageStack,
296: Rule[] rules, GetLegendGraphicRequest req) {
297:
298: Font labelFont = getLabelFont(req);
299: boolean useAA = false;
300: if (req.getLegendOptions().get("fontAntiAliasing") instanceof String) {
301: String aaVal = (String) req.getLegendOptions().get(
302: "fontAntiAliasing");
303: if (aaVal.equalsIgnoreCase("on")
304: || aaVal.equalsIgnoreCase("true")
305: || aaVal.equalsIgnoreCase("yes")
306: || aaVal.equalsIgnoreCase("1")) {
307: useAA = true;
308: }
309: }
310:
311: boolean forceLabelsOn = false;
312: boolean forceLabelsOff = false;
313: if (req.getLegendOptions().get("forceLabels") instanceof String) {
314: String forceLabelsOpt = (String) req.getLegendOptions()
315: .get("forceLabels");
316: if (forceLabelsOpt.equalsIgnoreCase("on")) {
317: forceLabelsOn = true;
318: } else if (forceLabelsOpt.equalsIgnoreCase("off")) {
319: forceLabelsOff = true;
320: }
321: }
322:
323: if (imageStack.size() == 0) {
324: throw new IllegalArgumentException(
325: "No legend graphics passed");
326: }
327:
328: final BufferedImage finalLegend;
329:
330: if (imageStack.size() == 1 && !forceLabelsOn) {
331: finalLegend = (BufferedImage) imageStack.get(0);
332: } else {
333: final int imgCount = imageStack.size();
334: final String[] labels = new String[imgCount];
335:
336: BufferedImage img = ((BufferedImage) imageStack.get(0));
337:
338: int totalHeight = 0;
339: int totalWidth = 0;
340: int[] rowHeights = new int[imgCount];
341:
342: for (int i = 0; i < imgCount; i++) {
343: img = (BufferedImage) imageStack.get(i);
344:
345: if (forceLabelsOff) {
346: totalWidth = (int) Math.ceil(Math.max(img
347: .getWidth(), totalWidth));
348: rowHeights[i] = img.getHeight();
349: totalHeight += img.getHeight();
350: } else {
351:
352: Rule rule = rules[i];
353:
354: //What's the label on this rule? We prefer to use
355: //the 'title' if it's available, but fall-back to 'name'
356: labels[i] = rule.getTitle();
357: if (labels[i] == null)
358: labels[i] = rule.getName();
359: if (labels[i] == null)
360: labels[i] = "";
361:
362: Graphics2D g = img.createGraphics();
363: g.setFont(labelFont);
364:
365: if (useAA) {
366: g.setRenderingHint(
367: RenderingHints.KEY_TEXT_ANTIALIASING,
368: RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
369: } else {
370: g
371: .setRenderingHint(
372: RenderingHints.KEY_TEXT_ANTIALIASING,
373: RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
374: }
375:
376: if (labels[i] != null && labels[i].length() > 0) {
377: final BufferedImage renderedLabel = renderLabel(
378: labels[i], g, req);
379: final Rectangle2D bounds = new Rectangle2D.Double(
380: 0, 0, renderedLabel.getWidth(),
381: renderedLabel.getHeight());
382:
383: totalWidth = (int) Math.ceil(Math.max(img
384: .getWidth()
385: + bounds.getWidth(), totalWidth));
386: rowHeights[i] = (int) Math.ceil(Math.max(img
387: .getHeight(), bounds.getHeight()));
388: } else {
389: totalWidth = (int) Math.ceil(Math.max(img
390: .getWidth(), totalWidth));
391: rowHeights[i] = (int) Math
392: .ceil(img.getHeight());
393: }
394: totalHeight += rowHeights[i];
395: }
396: }
397:
398: //buffer the width a bit
399: totalWidth += 2;
400:
401: final boolean transparent = req.isTransparent();
402: final Color backgroundColor = getBackgroundColor(req);
403: final Map hintsMap = new HashMap();
404: //create the final image
405: finalLegend = ImageUtils.createImage(totalWidth,
406: totalHeight, (IndexColorModel) null, transparent);
407: Graphics2D finalGraphics = ImageUtils
408: .prepareTransparency(transparent, backgroundColor,
409: finalLegend, hintsMap);
410:
411: int topOfRow = 0;
412:
413: for (int i = 0; i < imgCount; i++) {
414: img = (BufferedImage) imageStack.get(i);
415:
416: //draw the image
417: int y = topOfRow;
418:
419: if (img.getHeight() < rowHeights[i]) {
420: //move the image to the center of the row
421: y += (int) ((rowHeights[i] - img.getHeight()) / 2d);
422: }
423:
424: finalGraphics.drawImage(img, 0, y, imgObs);
425: if (forceLabelsOff) {
426: topOfRow += rowHeights[i];
427: continue;
428: }
429:
430: finalGraphics.setFont(labelFont);
431:
432: if (useAA) {
433: finalGraphics.setRenderingHint(
434: RenderingHints.KEY_TEXT_ANTIALIASING,
435: RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
436: } else {
437: finalGraphics.setRenderingHint(
438: RenderingHints.KEY_TEXT_ANTIALIASING,
439: RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
440: }
441:
442: //draw the label
443: if (labels[i] != null && labels[i].length() > 0) {
444: //first create the actual overall label image.
445: final BufferedImage renderedLabel = renderLabel(
446: labels[i], finalGraphics, req);
447:
448: y = topOfRow;
449:
450: if (renderedLabel.getHeight() < rowHeights[i]) {
451: y += (int) ((rowHeights[i] - renderedLabel
452: .getHeight()) / 2d);
453: }
454:
455: finalGraphics.drawImage(renderedLabel, img
456: .getWidth(), y, imgObs);
457: }
458:
459: topOfRow += rowHeights[i];
460: }
461: }
462:
463: return finalLegend;
464: }
465:
466: private static Font getLabelFont(GetLegendGraphicRequest req) {
467:
468: String legendFontName = "Sans-Serif";
469: String legendFontFamily = "plain";
470: int legendFontSize = 12;
471:
472: Map legendOptions = req.getLegendOptions();
473: if (legendOptions.get("fontName") != null) {
474: legendFontName = (String) legendOptions.get("fontName");
475: }
476: if (legendOptions.get("fontStyle") != null) {
477: legendFontFamily = (String) legendOptions.get("fontStyle");
478: }
479: if (legendOptions.get("fontSize") != null) {
480: try {
481: legendFontSize = Integer
482: .parseInt((String) legendOptions
483: .get("fontSize"));
484: } catch (Exception e) {
485: LOGGER
486: .warning("Error trying to interpret legendOption 'fontSize': "
487: + legendOptions.get("fontSize"));
488: }
489: }
490:
491: Font legendFont;
492: if (legendFontFamily.equalsIgnoreCase("italic")) {
493: legendFont = new Font(legendFontName, Font.ITALIC,
494: legendFontSize);
495: } else if (legendFontFamily.equalsIgnoreCase("bold")) {
496: legendFont = new Font(legendFontName, Font.BOLD,
497: legendFontSize);
498: } else {
499: legendFont = new Font(legendFontName, Font.PLAIN,
500: legendFontSize);
501: }
502:
503: return legendFont;
504: }
505:
506: private static Color getLabelFontColor(GetLegendGraphicRequest req) {
507: Map legendOptions = req.getLegendOptions();
508: String color = (String) legendOptions.get("fontColor");
509: if (color == null) {
510: //return the default
511: return FONT_COLOR;
512: }
513:
514: try {
515: return color(color);
516: } catch (NumberFormatException e) {
517: LOGGER.warning("Could not decode label color: " + color
518: + ", default to " + FONT_COLOR.toString());
519: return FONT_COLOR;
520: }
521: }
522:
523: /**
524: * Returns the image background color for the given
525: * {@link GetLegendGraphicRequest}.
526: *
527: * @param req
528: * @return the Color for the hexadecimal value passed as the
529: * <code>BGCOLOR</code>
530: * {@link GetLegendGraphicRequest#getLegendOptions() legend option},
531: * or the default background color if no bgcolor were passed.
532: */
533: private static Color getBackgroundColor(GetLegendGraphicRequest req) {
534: Map legendOptions = req.getLegendOptions();
535: String color = (String) legendOptions.get("bgColor");
536: if (color == null) {
537: //return the default
538: return BG_COLOR;
539: }
540:
541: try {
542: return color(color);
543: } catch (NumberFormatException e) {
544: LOGGER.warning("Could not decode background color: "
545: + color + ", default to " + BG_COLOR.toString());
546: return BG_COLOR;
547: }
548:
549: }
550:
551: private static Color color(String hex) throws NumberFormatException {
552: if (!hex.startsWith("#")) {
553: hex = "#" + hex;
554: }
555: return Color.decode(hex);
556: }
557:
558: /**
559: * Return a {@link BufferedImage} representing this label.
560: * The characters '\n' '\r' and '\f' are interpreted as linebreaks,
561: * as is the characater combination "\n" (as opposed to the actual '\n' character).
562: * This allows people to force line breaks in their labels by
563: * including the character "\" followed by "n" in their
564: * label.
565: *
566: * @param label - the label to render
567: * @param g - the Graphics2D that will be used to render this label
568: * @return a {@link BufferedImage} of the properly rendered label.
569: */
570: public static BufferedImage renderLabel(String label, Graphics2D g,
571: GetLegendGraphicRequest req) {
572: // We'll accept '/n' as a text string
573: //to indicate a line break, as well as a traditional 'real' line-break in the XML.
574: BufferedImage renderedLabel;
575: Color labelColor = getLabelFontColor(req);
576: if ((label.indexOf("\n") != -1) || (label.indexOf("\\n") != -1)) {
577: //this is a label WITH line-breaks...we need to figure out it's height *and*
578: //width, and then adjust the legend size accordingly
579: Rectangle2D bounds = new Rectangle2D.Double(0, 0, 0, 0);
580: ArrayList lineHeight = new ArrayList();
581: // four backslashes... "\\" -> '\', so "\\\\n" -> '\' + '\' + 'n'
582: final String realLabel = label.replaceAll("\\\\n", "\n");
583: StringTokenizer st = new StringTokenizer(realLabel,
584: "\n\r\f");
585:
586: while (st.hasMoreElements()) {
587: final String token = st.nextToken();
588: Rectangle2D this LineBounds = g.getFontMetrics()
589: .getStringBounds(token, g);
590:
591: //if this is directly added as thisLineBounds.getHeight(), then there are rounding errors
592: //because we can only DRAW fonts at discrete integer coords.
593: final int this LineHeight = (int) Math
594: .ceil(this LineBounds.getHeight());
595: bounds.add(0, this LineHeight + bounds.getHeight());
596: bounds.add(this LineBounds.getWidth(), 0);
597: lineHeight.add(new Integer((int) Math
598: .ceil(this LineBounds.getHeight())));
599: }
600:
601: //make the actual label image
602: renderedLabel = new BufferedImage((int) Math.ceil(bounds
603: .getWidth()), (int) Math.ceil(bounds.getHeight()),
604: BufferedImage.TYPE_INT_ARGB);
605:
606: st = new StringTokenizer(realLabel, "\n\r\f");
607:
608: Graphics2D rlg = renderedLabel.createGraphics();
609: rlg.setColor(labelColor);
610: rlg.setFont(g.getFont());
611: rlg
612: .setRenderingHint(
613: RenderingHints.KEY_TEXT_ANTIALIASING,
614: g
615: .getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING));
616:
617: int y = 0 - g.getFontMetrics().getDescent();
618: int c = 0;
619:
620: while (st.hasMoreElements()) {
621: y += ((Integer) lineHeight.get(c++)).intValue();
622: rlg.drawString(st.nextToken(), 0, y);
623: }
624: } else {
625: //this is a traditional 'regular-old' label. Just figure the
626: //size and act accordingly.
627: int height = (int) Math.ceil(g.getFontMetrics()
628: .getStringBounds(label, g).getHeight());
629: int width = (int) Math.ceil(g.getFontMetrics()
630: .getStringBounds(label, g).getWidth());
631: renderedLabel = new BufferedImage(width, height,
632: BufferedImage.TYPE_INT_ARGB);
633:
634: Graphics2D rlg = renderedLabel.createGraphics();
635: rlg.setColor(labelColor);
636: rlg.setFont(g.getFont());
637: rlg
638: .setRenderingHint(
639: RenderingHints.KEY_TEXT_ANTIALIASING,
640: g
641: .getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING));
642: rlg.drawString(label, 0, height
643: - rlg.getFontMetrics().getDescent());
644: }
645:
646: return renderedLabel;
647: }
648:
649: /**
650: * Returns a <code>java.awt.Shape</code> appropiate to render a legend
651: * graphic given the symbolizer type and the legend dimensions.
652: *
653: * @param symbolizer the Symbolizer for whose type a sample shape will be
654: * created
655: * @param legendWidth the requested width, in output units, of the legend
656: * graphic
657: * @param legendHeight the requested height, in output units, of the legend
658: * graphic
659: *
660: * @return an appropiate Line2D, Rectangle2D or LiteShape(Point) for the
661: * symbolizer, wether it is a LineSymbolizer, a PolygonSymbolizer,
662: * or a Point ot Text Symbolizer
663: *
664: * @throws IllegalArgumentException if an unknown symbolizer impl was
665: * passed in.
666: */
667: private LiteShape2 getSampleShape(Symbolizer symbolizer,
668: int legendWidth, int legendHeight) {
669: LiteShape2 sampleShape;
670: final float hpad = (legendWidth * hpaddingFactor);
671: final float vpad = (legendHeight * vpaddingFactor);
672:
673: if (symbolizer instanceof LineSymbolizer) {
674: if (this .sampleLine == null) {
675: Coordinate[] coords = {
676: new Coordinate(hpad, legendHeight - vpad),
677: new Coordinate(legendWidth - hpad, vpad) };
678: LineString geom = geomFac.createLineString(coords);
679:
680: try {
681: this .sampleLine = new LiteShape2(geom, null, null,
682: false);
683: } catch (Exception e) {
684: this .sampleLine = null;
685: }
686: }
687:
688: sampleShape = this .sampleLine;
689: } else if ((symbolizer instanceof PolygonSymbolizer)
690: || (symbolizer instanceof RasterSymbolizer)) {
691: if (this .sampleRect == null) {
692: final float w = legendWidth - (2 * hpad);
693: final float h = legendHeight - (2 * vpad);
694:
695: Coordinate[] coords = { new Coordinate(hpad, vpad),
696: new Coordinate(hpad, vpad + h),
697: new Coordinate(hpad + w, vpad + h),
698: new Coordinate(hpad + w, vpad),
699: new Coordinate(hpad, vpad) };
700: LinearRing shell = geomFac.createLinearRing(coords);
701: Polygon geom = geomFac.createPolygon(shell, null);
702:
703: try {
704: this .sampleRect = new LiteShape2(geom, null, null,
705: false);
706: } catch (Exception e) {
707: this .sampleRect = null;
708: }
709: }
710:
711: sampleShape = this .sampleRect;
712: } else if (symbolizer instanceof PointSymbolizer
713: || symbolizer instanceof TextSymbolizer) {
714: if (this .samplePoint == null) {
715: Coordinate coord = new Coordinate(legendWidth / 2,
716: legendHeight / 2);
717:
718: try {
719: this .samplePoint = new LiteShape2(geomFac
720: .createPoint(coord), null, null, false);
721: } catch (Exception e) {
722: this .samplePoint = null;
723: }
724: }
725:
726: sampleShape = this .samplePoint;
727: } else {
728: throw new IllegalArgumentException("Unknown symbolizer: "
729: + symbolizer);
730: }
731:
732: return sampleShape;
733: }
734:
735: /**
736: * Creates a sample Feature instance in the hope that it can be used in the
737: * rendering of the legend graphic.
738: *
739: * @param schema the schema for which to create a sample Feature instance
740: *
741: * @return
742: *
743: * @throws WmsException
744: */
745: private Feature createSampleFeature(FeatureType schema)
746: throws WmsException {
747: Feature sampleFeature;
748:
749: try {
750: AttributeType[] atts = schema.getAttributeTypes();
751: Object[] attributes = new Object[atts.length];
752:
753: for (int i = 0; i < atts.length; i++)
754: attributes[i] = atts[i].createDefaultValue();
755:
756: sampleFeature = schema.create(attributes);
757: } catch (IllegalAttributeException e) {
758: e.printStackTrace();
759: throw new WmsException(e);
760: }
761:
762: return sampleFeature;
763: }
764:
765: /**
766: * Finds the applicable Rules for the given scale denominator.
767: *
768: * @param ftStyles
769: * @param scaleDenominator
770: *
771: * @return
772: */
773: private Rule[] getApplicableRules(FeatureTypeStyle[] ftStyles,
774: double scaleDenominator) {
775: /**
776: * Holds both the rules that apply and the ElseRule's if any, in the
777: * order they appear
778: */
779: final List ruleList = new ArrayList();
780:
781: // get applicable rules at the current scale
782: for (int i = 0; i < ftStyles.length; i++) {
783: FeatureTypeStyle fts = ftStyles[i];
784: Rule[] rules = fts.getRules();
785:
786: for (int j = 0; j < rules.length; j++) {
787: Rule r = rules[j];
788:
789: if (isWithInScale(r, scaleDenominator)) {
790: ruleList.add(r);
791:
792: /*
793: * I'm commented this out since I guess it has no sense
794: * for producing the legend, since wether or not the rule
795: * has an else filter, the legend is drawn only if the
796: * scale denominator lies inside the rule's scale range.
797: if (r.hasElseFilter()) {
798: ruleList.add(r);
799: }
800: */
801: }
802: }
803: }
804:
805: return (Rule[]) ruleList.toArray(new Rule[ruleList.size()]);
806: }
807:
808: /**
809: * Checks if a rule can be triggered at the current scale level
810: *
811: * @param r The rule
812: * @param scaleDenominator the scale denominator to check if it is between
813: * the rule's scale range. -1 means that it allways is.
814: *
815: * @return true if the scale is compatible with the rule settings
816: */
817: private boolean isWithInScale(Rule r, double scaleDenominator) {
818: return (scaleDenominator == -1)
819: || (((r.getMinScaleDenominator() - TOLERANCE) <= scaleDenominator) && ((r
820: .getMaxScaleDenominator() + TOLERANCE) > scaleDenominator));
821: }
822:
823: /**
824: * DOCUMENT ME!
825: *
826: * @return
827: *
828: * @throws IllegalStateException DOCUMENT ME!
829: */
830: public BufferedImage getLegendGraphic() {
831: if (this .legendGraphic == null) {
832: throw new IllegalStateException();
833: }
834:
835: return this .legendGraphic;
836: }
837:
838: /**
839: * Asks the rendering to stop processing.
840: */
841: public void abort() {
842: this .renderingStopRequested = true;
843: }
844: }
|