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.Color;
008: import java.awt.Graphics2D;
009: import java.awt.Rectangle;
010: import java.awt.RenderingHints;
011: import java.awt.image.BufferedImage;
012: import java.awt.image.IndexColorModel;
013: import java.awt.image.RenderedImage;
014: import java.io.OutputStream;
015: import java.util.Arrays;
016: import java.util.HashMap;
017: import java.util.List;
018: import java.util.Map;
019: import java.util.logging.Level;
020: import java.util.logging.Logger;
021:
022: import javax.media.jai.ImageLayout;
023: import javax.media.jai.Interpolation;
024: import javax.media.jai.InterpolationBicubic2;
025: import javax.media.jai.InterpolationBilinear;
026: import javax.media.jai.InterpolationNearest;
027: import javax.media.jai.JAI;
028: import javax.media.jai.LookupTableJAI;
029: import javax.media.jai.operator.LookupDescriptor;
030:
031: import org.geotools.feature.Feature;
032: import org.geotools.geometry.jts.ReferencedEnvelope;
033: import org.geotools.map.MapLayer;
034: import org.geotools.renderer.RenderListener;
035: import org.geotools.renderer.shape.ShapefileRenderer;
036: import org.opengis.feature.simple.SimpleFeature;
037: import org.vfny.geoserver.config.WMSConfig;
038: import org.vfny.geoserver.global.WMS;
039: import org.vfny.geoserver.wms.RasterMapProducer;
040: import org.vfny.geoserver.wms.WmsException;
041: import org.vfny.geoserver.wms.requests.GetMapRequest;
042: import org.vfny.geoserver.wms.responses.map.metatile.MetatileMapProducer;
043: import org.vfny.geoserver.wms.responses.palette.InverseColorMapOp;
044:
045: /**
046: * Abstract base class for GetMapProducers that relies in LiteRenderer for
047: * creating the raster map and then outputs it in the format they specializes
048: * in.
049: *
050: * <p>
051: * This class does the job of producing a BufferedImage using geotools
052: * LiteRenderer, so it should be enough for a subclass to implement
053: * {@linkPlain #formatImageOutputStream(String, BufferedImage, OutputStream)}
054: * </p>
055: *
056: * <p>
057: * Generates a map using the geotools jai rendering classes. Uses the Lite
058: * renderer, loading the data on the fly, which is quite nice. Thanks Andrea and
059: * Gabriel. The word is that we should eventually switch over to
060: * StyledMapRenderer and do some fancy stuff with caching layers, but I think we
061: * are a ways off with its maturity to try that yet. So Lite treats us quite
062: * well, as it is stateless and therefore loads up nice and fast.
063: * </p>
064: *
065: * <p>
066: * </p>
067: *
068: * @author Chris Holmes, TOPP
069: * @author Simone Giannecchini, GeoSolutions
070: * @version $Id: DefaultRasterMapProducer.java 7925 2007-12-04 15:28:15Z aaime $
071: */
072: public abstract class DefaultRasterMapProducer extends
073: AbstractRasterMapProducer implements RasterMapProducer {
074: private final static Interpolation NN_INTERPOLATION = new InterpolationNearest();
075:
076: private final static Interpolation BIL_INTERPOLATION = new InterpolationBilinear();
077:
078: private final static Interpolation BIC_INTERPOLATION = new InterpolationBicubic2(
079: 0);
080:
081: // antialiasing settings, no antialias, only text, full antialias
082: private final static String AA_NONE = "NONE";
083:
084: private final static String AA_TEXT = "TEXT";
085:
086: private final static String AA_FULL = "FULL";
087:
088: private final static List AA_SETTINGS = Arrays.asList(new String[] {
089: AA_NONE, AA_TEXT, AA_FULL });
090:
091: /**
092: * The lookup table used for data type transformation (it's really the
093: * identity one)
094: */
095: private static LookupTableJAI IDENTITY_TABLE = new LookupTableJAI(
096: getTable());
097:
098: private static byte[] getTable() {
099: byte[] arr = new byte[256];
100: for (int i = 0; i < arr.length; i++) {
101: arr[i] = (byte) i;
102: }
103: return arr;
104: }
105:
106: /** WMS Service configuration * */
107: private WMS wms;
108:
109: /** A logger for this class. */
110: private static final Logger LOGGER = org.geotools.util.logging.Logging
111: .getLogger("org.vfny.geoserver.responses.wms.map");
112:
113: /** Which format to encode the image in if one is not supplied */
114: private static final String DEFAULT_MAP_FORMAT = "image/png";
115:
116: /**
117: *
118: */
119: public DefaultRasterMapProducer() {
120: this (DEFAULT_MAP_FORMAT, null);
121: }
122:
123: /**
124: *
125: */
126: public DefaultRasterMapProducer(WMS wms) {
127: this (DEFAULT_MAP_FORMAT, wms);
128: }
129:
130: /**
131: *
132: */
133: public DefaultRasterMapProducer(String outputFormat, WMS wms) {
134: this (outputFormat, outputFormat, wms);
135: }
136:
137: /**
138: *
139: */
140: public DefaultRasterMapProducer(String outputFormat, String mime,
141: WMS wms) {
142: super (outputFormat, mime);
143: this .wms = wms;
144: }
145:
146: /**
147: * Writes the image to the client.
148: *
149: * @param out
150: * The output stream to write to.
151: *
152: * @throws org.vfny.geoserver.ServiceException
153: * DOCUMENT ME!
154: * @throws java.io.IOException
155: * DOCUMENT ME!
156: */
157: public void writeTo(OutputStream out)
158: throws org.vfny.geoserver.ServiceException,
159: java.io.IOException {
160: formatImageOutputStream(this .image, out);
161: }
162:
163: /**
164: * Performs the execute request using geotools rendering.
165: *
166: * @param map
167: * The information on the types requested.
168: *
169: * @throws WmsException
170: * For any problems.
171: */
172: public void produceMap() throws WmsException {
173:
174: final int width = mapContext.getMapWidth();
175: final int height = mapContext.getMapHeight();
176:
177: if (LOGGER.isLoggable(Level.FINE)) {
178: LOGGER.fine(new StringBuffer("setting up ").append(width)
179: .append("x").append(height).append(" image")
180: .toString());
181: }
182:
183: // extra antialias setting
184: final GetMapRequest request = mapContext.getRequest();
185: String antialias = (String) request.getFormatOptions().get(
186: "antialias");
187: if (antialias != null)
188: antialias = antialias.toUpperCase();
189:
190: // figure out a palette for buffered image creation
191: IndexColorModel palette = null;
192: final InverseColorMapOp paletteInverter = mapContext
193: .getPaletteInverter();
194: final boolean transparent = mapContext.isTransparent();
195: final Color bgColor = mapContext.getBgColor();
196: if (paletteInverter != null && AA_NONE.equals(antialias)) {
197: palette = paletteInverter.getIcm();
198: } else if (AA_NONE.equals(antialias)) {
199: PaletteExtractor pe = new PaletteExtractor(
200: transparent ? null : bgColor);
201: MapLayer[] layers = mapContext.getLayers();
202: for (int i = 0; i < layers.length; i++) {
203: pe.visit(layers[i].getStyle());
204: if (!pe.canComputePalette())
205: break;
206: }
207: if (pe.canComputePalette())
208: palette = pe.getPalette();
209: }
210:
211: // we use the alpha channel if the image is transparent or if the meta
212: // tiler
213: // is enabled, since apparently the Crop operation inside the meta-tiler
214: // generates striped images in that case (see GEOS-
215: boolean useAlpha = transparent
216: || MetatileMapProducer.isRequestTiled(request, this );
217: final RenderedImage preparedImage = prepareImage(width, height,
218: palette, useAlpha);
219: final Map hintsMap = new HashMap();
220:
221: final Graphics2D graphic = ImageUtils.prepareTransparency(
222: transparent, bgColor, preparedImage, hintsMap);
223:
224: // set up the antialias hints
225: if (AA_NONE.equals(antialias)) {
226: hintsMap.put(RenderingHints.KEY_ANTIALIASING,
227: RenderingHints.VALUE_ANTIALIAS_OFF);
228: if (preparedImage.getColorModel() instanceof IndexColorModel) {
229: // otherwise we end up with dithered colors where the match is
230: // not 100%
231: hintsMap.put(RenderingHints.KEY_DITHERING,
232: RenderingHints.VALUE_DITHER_DISABLE);
233: }
234: } else if (AA_TEXT.equals(antialias)) {
235: hintsMap.put(RenderingHints.KEY_ANTIALIASING,
236: RenderingHints.VALUE_ANTIALIAS_OFF);
237: hintsMap.put(RenderingHints.KEY_TEXT_ANTIALIASING,
238: RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
239: } else {
240: if (antialias != null && !AA_FULL.equals(antialias)) {
241: LOGGER.warning("Unrecognized antialias setting '"
242: + antialias + "', valid values are "
243: + AA_SETTINGS);
244: }
245: hintsMap.put(RenderingHints.KEY_ANTIALIASING,
246: RenderingHints.VALUE_ANTIALIAS_ON);
247: }
248:
249: // turn off/on interpolation rendering hint
250: if ((wms != null)
251: && WMSConfig.INT_NEAREST.equals(wms
252: .getAllowInterpolation())) {
253: hintsMap.put(JAI.KEY_INTERPOLATION, NN_INTERPOLATION);
254: } else if ((wms != null)
255: && WMSConfig.INT_BIlINEAR.equals(wms
256: .getAllowInterpolation())) {
257: hintsMap.put(JAI.KEY_INTERPOLATION, BIL_INTERPOLATION);
258: } else if ((wms != null)
259: && WMSConfig.INT_BICUBIC.equals(wms
260: .getAllowInterpolation())) {
261: hintsMap.put(JAI.KEY_INTERPOLATION, BIC_INTERPOLATION);
262: }
263: // line look better with this hint, they are less blurred
264: hintsMap.put(RenderingHints.KEY_STROKE_CONTROL,
265: RenderingHints.VALUE_STROKE_NORMALIZE);
266:
267: // make sure the hints are set before we start rendering the map
268: graphic.setRenderingHints(hintsMap);
269:
270: Rectangle paintArea = new Rectangle(width, height);
271: RenderingHints hints = new RenderingHints(hintsMap);
272: renderer = new ShapefileRenderer();
273: renderer.setContext(mapContext);
274: renderer.setJava2DHints(hints);
275: // shapefile renderer won't log rendering errors, sigh, we have to do it
276: // manually
277: if (renderer instanceof ShapefileRenderer
278: && LOGGER.isLoggable(Level.FINE)) {
279: renderer.addRenderListener(new RenderListener() {
280:
281: public void featureRenderer(Feature feature) {
282: }
283:
284: public void errorOccurred(Exception e) {
285: LOGGER.log(Level.FINE, "Rendering error occurred",
286: e);
287: }
288:
289: });
290: }
291:
292: // setup the renderer hints
293: Map rendererParams = new HashMap();
294: rendererParams.put("optimizedDataLoadingEnabled", new Boolean(
295: true));
296: rendererParams.put("renderingBuffer", new Integer(mapContext
297: .getBuffer()));
298: rendererParams.put("maxFiltersToSendToDatastore", new Integer(
299: 20));
300: rendererParams.put(
301: ShapefileRenderer.SCALE_COMPUTATION_METHOD_KEY,
302: ShapefileRenderer.SCALE_OGC);
303: if (AA_NONE.equals(antialias)) {
304: rendererParams.put(ShapefileRenderer.TEXT_RENDERING_KEY,
305: ShapefileRenderer.TEXT_RENDERING_STRING);
306: } else {
307: rendererParams.put(ShapefileRenderer.TEXT_RENDERING_KEY,
308: ShapefileRenderer.TEXT_RENDERING_OUTLINE);
309: }
310: renderer.setRendererHints(rendererParams);
311:
312: // if abort already requested bail out
313: if (this .abortRequested) {
314: graphic.dispose();
315: return;
316: }
317:
318: // finally render the image
319: final ReferencedEnvelope dataArea = mapContext
320: .getAreaOfInterest();
321: renderer.paint(graphic, paintArea, dataArea);
322: graphic.dispose();
323: if (!this .abortRequested) {
324: if (palette != null && palette.getMapSize() < 256)
325: this .image = optimizeSampleModel(preparedImage);
326: else
327: this .image = preparedImage;
328: }
329: }
330:
331: /**
332: * Sets up a {@link BufferedImage#TYPE_4BYTE_ABGR} if the paletteInverter is
333: * not provided, or a indexed image otherwise. Subclasses may override this
334: * method should they need a special kind of image
335: *
336: * @param width
337: * @param height
338: * @param paletteInverter
339: * @return
340: */
341: protected RenderedImage prepareImage(int width, int height,
342: IndexColorModel palette, boolean transparent) {
343: return ImageUtils.createImage(width, height, palette,
344: transparent);
345: }
346:
347: /**
348: * @param originalImage
349: * @return
350: */
351: protected RenderedImage forceIndexed8Bitmask(
352: RenderedImage originalImage) {
353: return ImageUtils.forceIndexed8Bitmask(originalImage,
354: mapContext.getPaletteInverter());
355: }
356:
357: /**
358: * This takes an image with an indexed color model that uses less than 256
359: * colors and has a 8bit sample model, and transforms it to one that has the
360: * optimal sample model (for example, 1bit if the palette only has 2 colors)
361: *
362: * @param source
363: * @return
364: */
365: private RenderedImage optimizeSampleModel(RenderedImage source) {
366: int w = source.getWidth();
367: int h = source.getHeight();
368: ImageLayout layout = new ImageLayout();
369: layout.setColorModel(source.getColorModel());
370: layout.setSampleModel(source.getColorModel()
371: .createCompatibleSampleModel(w, h));
372: // if I don't force tiling off with this setting an exception is thrown
373: // when writing the image out...
374: layout.setTileWidth(w);
375: layout.setTileHeight(h);
376: RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT,
377: layout);
378: return LookupDescriptor.create(source, IDENTITY_TABLE, hints);
379: }
380:
381: }
|