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.map.kml;
006:
007: import com.vividsolutions.jts.geom.Envelope;
008: import org.geotools.data.DataUtilities;
009: import org.geotools.data.DefaultQuery;
010: import org.geotools.data.FeatureSource;
011: import org.geotools.data.Query;
012: import org.geotools.factory.CommonFactoryFinder;
013: import org.geotools.factory.GeoTools;
014: import org.geotools.feature.AttributeType;
015: import org.geotools.feature.FeatureCollection;
016: import org.geotools.feature.FeatureType;
017: import org.geotools.feature.GeometryAttributeType;
018: import org.geotools.filter.IllegalFilterException;
019: import org.geotools.geometry.jts.ReferencedEnvelope;
020: import org.geotools.image.ImageWorker;
021: import org.geotools.map.MapContext;
022: import org.geotools.map.MapLayer;
023: import org.geotools.referencing.CRS;
024: import org.geotools.renderer.lite.RendererUtilities;
025: import org.geotools.renderer.lite.StreamingRenderer;
026: import org.opengis.filter.Filter;
027: import org.opengis.filter.FilterFactory;
028: import org.opengis.referencing.crs.CoordinateReferenceSystem;
029: import org.vfny.geoserver.wms.WMSMapContext;
030: import java.awt.AlphaComposite;
031: import java.awt.Color;
032: import java.awt.Graphics2D;
033: import java.awt.Rectangle;
034: import java.awt.RenderingHints;
035: import java.awt.geom.AffineTransform;
036: import java.awt.image.BufferedImage;
037: import java.io.IOException;
038: import java.io.OutputStream;
039: import java.util.ArrayList;
040: import java.util.HashMap;
041: import java.util.List;
042: import java.util.Map;
043: import java.util.logging.Level;
044: import java.util.logging.Logger;
045: import java.util.zip.ZipEntry;
046: import java.util.zip.ZipOutputStream;
047: import javax.media.jai.GraphicsJAI;
048:
049: /**
050: * @deprecated use {@link KMLTransformer}.
051: */
052: public class EncodeKML {
053: /** Standard Logger */
054: private static final Logger LOGGER = org.geotools.util.logging.Logging
055: .getLogger("org.vfny.geoserver.responses.wms.map.kml");
056:
057: /** Filter factory for creating bounding box filters */
058: //private FilterFactory filterFactory = FilterFactoryFinder.createFilterFactory();
059: /** the XML and KML header */
060: private static final String KML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\t"
061: + "<kml xmlns=\"http://earth.google.com/kml/2.0\">\n";
062:
063: /** the KML closing element */
064: private static final String KML_FOOTER = "</kml>\n";
065:
066: /**
067: * Map context document - layers, styles aoi etc.
068: *
069: * @uml.property name="mapContext"
070: * @uml.associationEnd multiplicity="(1 1)"
071: */
072: private WMSMapContext mapContext;
073:
074: /**
075: * Actualy writes the KML out
076: *
077: * @uml.property name="writer"
078: * @uml.associationEnd multiplicity="(0 1)"
079: */
080: private KMLWriter writer;
081:
082: /** Filter factory for creating bounding box filters */
083: private FilterFactory filterFactory = CommonFactoryFinder
084: .getFilterFactory2(GeoTools.getDefaultHints());
085:
086: /** Flag to be monotored by writer loops */
087: private boolean abortProcess;
088:
089: /**
090: * Creates a new EncodeKML object.
091: *
092: * @param mapContext A full description of the map to be encoded.
093: */
094: public EncodeKML(WMSMapContext mapContext) {
095: this .mapContext = mapContext;
096: }
097:
098: /**
099: * Sets the abort flag. Active encoding may be halted, but this is not garanteed.
100: */
101: public void abort() {
102: abortProcess = true;
103: }
104:
105: /**
106: * Perform the actual encoding. May return early if abort it called.
107: *
108: * @param out Ouput stream to send the data to.
109: *
110: * @throws IOException Thrown if anything goes wrong whilst writing
111: */
112: public void encodeKML(final OutputStream out) throws IOException {
113: this .writer = new KMLWriter(out, mapContext);
114: //once KML supports bbox queries against WMS this can be used to
115: //decimate the geometries based on zoom level.
116: //writer.setMinCoordDistance(env.getWidth() / 1000);
117: abortProcess = false;
118:
119: long t = System.currentTimeMillis();
120:
121: try {
122: writeHeader();
123:
124: ArrayList layerRenderList = new ArrayList(); // not used in straight KML generation
125: writeLayers(false, layerRenderList);
126: writeFooter();
127:
128: this .writer.flush();
129: t = System.currentTimeMillis() - t;
130: LOGGER.fine(new StringBuffer("KML generated, it took")
131: .append(t).append(" ms").toString());
132: } catch (IOException ioe) {
133: if (abortProcess) {
134: LOGGER.fine("KML encoding aborted");
135:
136: return;
137: } else {
138: throw ioe;
139: }
140: } catch (AbortedException ex) {
141: return;
142: }
143: }
144:
145: /**
146: * This method is used to encode kml + images and put all the stuff into a KMZ
147: * file.
148: *
149: * @param out the response is a Zipped output stream
150: * @throws IOException
151: */
152: public void encodeKMZ(final ZipOutputStream out) throws IOException {
153: this .writer = new KMLWriter(out, mapContext);
154:
155: abortProcess = false;
156:
157: long t = System.currentTimeMillis();
158:
159: try {
160: // first we produce the KML file containing the code and the PlaceMarks
161: final ZipEntry e = new ZipEntry("wms.kml");
162: out.putNextEntry(e);
163: writeHeader();
164:
165: ArrayList layerRenderList = new ArrayList();
166: writeLayers(true, layerRenderList);
167: writeFooter();
168: this .writer.flush();
169: out.closeEntry();
170:
171: // then we produce and store all the layer images
172: writeImages(out, layerRenderList);
173:
174: t = System.currentTimeMillis() - t;
175: LOGGER.fine(new StringBuffer("KMZ generated, it took")
176: .append(t).append(" ms").toString());
177: } catch (IOException ioe) {
178: if (abortProcess) {
179: LOGGER.fine("KMZ encoding aborted");
180:
181: return;
182: } else {
183: throw ioe;
184: }
185: } catch (AbortedException ex) {
186: return;
187: }
188: }
189:
190: /**
191: * Determines whether to return a vector (KML) result of the data or to
192: * return an image instead.
193: * If the kmscore is 100, then the output should always be vector. If
194: * the kmscore is 0, it should always be raster. In between, the number of
195: * features is weighed against the kmscore value.
196: * kmscore determines whether to return the features as vectors, or as one
197: * raster image. It is the point, determined by the user, where X number of
198: * features is "too many" and the result should be returned as an image instead.
199: *
200: * kmscore is logarithmic. The higher the value, the more features it takes
201: * to make the algorithm return an image. The lower the kmscore, the fewer
202: * features it takes to force an image to be returned.
203: * (in use, the formula is exponential: as you increase the KMScore value,
204: * the number of features required increases exponentially).
205: *
206: * @param kmscore the score, between 0 and 100, use to determine what output to use
207: * @param numFeatures how many features are being rendered
208: * @return true: use just kml vectors, false: use raster result
209: */
210: private boolean useVectorOutput(int kmscore, int numFeatures) {
211: if (kmscore == 100) {
212: return true; // vector KML
213: }
214:
215: if (kmscore == 0) {
216: return false; // raster KMZ
217: }
218:
219: // For numbers in between, determine exponentionally based on kmscore value:
220: // 10^(kmscore/15)
221: // This results in exponential growth.
222: // The lowest bound is 1 feature and the highest bound is 3.98 million features
223: // The most useful kmscore values are between 20 and 70 (21 and 46000 features respectively)
224: // A good default kmscore value is around 40 (464 features)
225: double magic = Math.pow(10, kmscore / 15);
226:
227: if (numFeatures > magic) {
228: return false; // return raster
229: } else {
230: return true; // return vector
231: }
232: }
233:
234: /**
235: * writes out standard KML header
236: *
237: * @throws IOException
238: */
239: private void writeHeader() throws IOException {
240: writer.write(KML_HEADER);
241: }
242:
243: /**
244: * writes out standard KML footer
245: *
246: * @throws IOException DOCUMENT ME!
247: */
248: private void writeFooter() throws IOException {
249: writer.write(KML_FOOTER);
250: }
251:
252: /**
253: * Processes each of the layers within the current mapContext in turn.
254: *
255: * writeLayers must be called before writeImages in order for the kmScore
256: * algorithm to work.
257: *
258: * @throws IOException
259: * @throws AbortedException
260: *
261: */
262: private void writeLayers(final boolean kmz,
263: ArrayList layerRenderList) throws IOException,
264: AbortedException {
265: MapLayer[] layers = mapContext.getLayers();
266: int nLayers = layers.length;
267:
268: final int imageWidth = this .mapContext.getMapWidth();
269: final int imageHeight = this .mapContext.getMapHeight();
270:
271: //final CoordinateReferenceSystem requestedCrs = mapContext.getCoordinateReferenceSystem();
272: //writer.setRequestedCRS(requestedCrs);
273: //writer.setScreenSize(new Rectangle(imageWidth, imageHeight));
274: if (nLayers > 1) { // if we have more than one layer, use the name "GeoServer" to group them
275: writer.startDocument("GeoServer", null);
276: }
277:
278: for (int i = 0; i < nLayers; i++) { // for every layer specified in the request
279:
280: MapLayer layer = layers[i];
281: writer.startDocument(layer.getTitle(), null);
282:
283: //FeatureReader featureReader = null;
284: FeatureSource fSource = layer.getFeatureSource();
285: FeatureType schema = fSource.getSchema();
286:
287: //GeometryAttributeType geometryAttribute = schema.getDefaultGeometry();
288: //CoordinateReferenceSystem sourceCrs = geometryAttribute.getCoordinateSystem();
289: Rectangle paintArea = new Rectangle(imageWidth, imageHeight);
290: AffineTransform worldToScreen = RendererUtilities
291: .worldToScreenTransform(mapContext
292: .getAreaOfInterest(), paintArea);
293: double scaleDenominator = 1;
294:
295: try {
296: scaleDenominator = RendererUtilities.calculateScale(
297: mapContext.getAreaOfInterest(), mapContext
298: .getCoordinateReferenceSystem(),
299: paintArea.width, paintArea.height, 90); // 90 = OGC standard DPI (see SLD spec page 37)
300: } catch (Exception e) // probably either (1) no CRS (2) error xforming
301: {
302: scaleDenominator = 1 / worldToScreen.getScaleX(); //DJB old method - the best we can do
303: }
304:
305: writer.setRequestedScale(scaleDenominator);
306:
307: String[] attributes;
308: boolean isRaster = false;
309:
310: AttributeType[] ats = schema.getAttributeTypes();
311: final int length = ats.length;
312: attributes = new String[length];
313:
314: for (int t = 0; t < length; t++) {
315: attributes[t] = ats[t].getName();
316:
317: if (attributes[t].equals("grid")) {
318: isRaster = true;
319: }
320: }
321:
322: try {
323: CoordinateReferenceSystem sourceCrs = schema
324: .getDefaultGeometry().getCoordinateSystem();
325: writer.setSourceCrs(sourceCrs); // it seems to work better getting it from the schema, here
326:
327: Envelope envelope = mapContext.getAreaOfInterest();
328: ReferencedEnvelope aoi = new ReferencedEnvelope(
329: envelope, mapContext
330: .getCoordinateReferenceSystem());
331:
332: Filter filter = null;
333:
334: //ReferencedEnvelope aoi = mapContext.getAreaOfInterest();
335: if (!CRS.equalsIgnoreMetadata(aoi
336: .getCoordinateReferenceSystem(), schema
337: .getDefaultGeometry().getCoordinateSystem())) {
338: aoi = aoi.transform(schema.getDefaultGeometry()
339: .getCoordinateSystem(), true);
340: }
341:
342: filter = createBBoxFilters(schema, attributes, aoi);
343:
344: // now build the query using only the attributes and the bounding
345: // box needed
346: DefaultQuery q = new DefaultQuery(schema.getTypeName());
347: q.setFilter(filter);
348: q.setPropertyNames(attributes);
349:
350: // now, if a definition query has been established for this layer, be
351: // sure to respect it by combining it with the bounding box one.
352: Query definitionQuery = layer.getQuery();
353:
354: if (definitionQuery != Query.ALL) {
355: if (q == Query.ALL) {
356: q = (DefaultQuery) definitionQuery;
357: } else {
358: q = (DefaultQuery) DataUtilities.mixQueries(
359: definitionQuery, q, "KMLEncoder");
360: }
361: }
362:
363: q.setCoordinateSystem(layer.getFeatureSource()
364: .getSchema().getDefaultGeometry()
365: .getCoordinateSystem());
366:
367: FeatureCollection fc = fSource.getFeatures(q);
368:
369: int kmscore = mapContext.getRequest().getKMScore(); //KMZ score value
370: boolean useVector = useVectorOutput(kmscore, fc.size()); // kmscore = render vector/raster
371:
372: if (useVector || !kmz) {
373: LOGGER.info("Layer (" + layer.getTitle()
374: + ") rendered with KML vector output.");
375: layerRenderList.add(new Integer(i)); // save layer number so it won't be rendered
376:
377: if (!isRaster) {
378: writer.writeFeaturesAsVectors(fc, layer); // vector
379: } else {
380: writer.writeCoverages(fc, layer); // coverage
381: }
382: } else {
383: // user requested KMZ and kmscore says render raster
384: LOGGER.info("Layer (" + layer.getTitle()
385: + ") rendered with KMZ raster output.");
386: // layer order is only needed for raster results. In the <GroundOverlay> tag
387: // you need to point to a raster image, this image has the layer number as
388: // part of the name. The kml will then reference the image via the layer number
389: writer.writeFeaturesAsRaster(fc, layer, i); // raster
390: }
391:
392: LOGGER.fine("finished writing");
393: } catch (IOException ex) {
394: LOGGER.info(new StringBuffer("process failed: ")
395: .append(ex.getMessage()).toString());
396: throw ex;
397: } catch (AbortedException ae) {
398: LOGGER.info(new StringBuffer("process aborted: ")
399: .append(ae.getMessage()).toString());
400: throw ae;
401: } catch (Throwable t) {
402: LOGGER.warning(new StringBuffer("UNCAUGHT exception: ")
403: .append(t.getMessage()).toString());
404:
405: IOException ioe = new IOException(new StringBuffer(
406: "UNCAUGHT exception: ").append(t.getMessage())
407: .toString());
408: ioe.setStackTrace(t.getStackTrace());
409: throw ioe;
410: } finally {
411: /*if (featureReader != null) {
412: try{
413: featureReader.close();
414: }catch(IOException ioe){
415: //featureReader was probably closed already.
416: }
417: }*/
418: }
419:
420: writer.endDocument();
421: }
422:
423: if (nLayers > 1) {
424: writer.endDocument();
425: }
426: }
427:
428: /**
429: * This method produces and stores PNG images of all map layers using the StreamingRenderer and JAI Encoder.
430: *
431: * @param outZ
432: * @throws IOException
433: * @throws AbortedException
434: */
435: private void writeImages(final ZipOutputStream outZ,
436: ArrayList layerRenderList) throws IOException,
437: AbortedException {
438: MapLayer[] layers = this .mapContext.getLayers();
439: int nLayers = layers.length;
440:
441: for (int i = 0; i < nLayers; i++) {
442: if (layerRenderList.size() > 0) {
443: int num = ((Integer) layerRenderList.get(0)).intValue();
444:
445: if (num == i) { // if this layer is a layer that doesn't need to be rendered, move to next layer
446: layerRenderList.remove(0);
447:
448: continue;
449: }
450: }
451:
452: final MapLayer layer = layers[i];
453: MapContext map = this .mapContext;
454: map.clearLayerList();
455: map.addLayer(layer);
456:
457: final int width = this .mapContext.getMapWidth();
458: final int height = this .mapContext.getMapHeight();
459:
460: LOGGER.fine(new StringBuffer("setting up ").append(width)
461: .append("x").append(height).append(" image")
462: .toString());
463:
464: // simone: ARGB should be much better
465: BufferedImage curImage = new BufferedImage(width, height,
466: BufferedImage.TYPE_4BYTE_ABGR);
467:
468: // simboss: this should help out with coverages
469: final Graphics2D graphic = GraphicsJAI.createGraphicsJAI(
470: curImage.createGraphics(), null);
471:
472: LOGGER.fine("setting to transparent");
473:
474: int type = AlphaComposite.SRC;
475: graphic.setComposite(AlphaComposite.getInstance(type));
476:
477: Color c = new Color(this .mapContext.getBgColor().getRed(),
478: this .mapContext.getBgColor().getGreen(),
479: this .mapContext.getBgColor().getBlue(), 0);
480:
481: //LOGGER.info("****** bg color: "+c.getRed()+","+c.getGreen()+","+c.getBlue()+","+c.getAlpha()+", trans: "+c.getTransparency());
482: graphic.setBackground(this .mapContext.getBgColor());
483: graphic.setColor(c);
484: graphic.fillRect(0, 0, width, height);
485:
486: type = AlphaComposite.SRC_OVER;
487: graphic.setComposite(AlphaComposite.getInstance(type));
488:
489: Rectangle paintArea = new Rectangle(width, height);
490:
491: final StreamingRenderer renderer = new StreamingRenderer();
492: renderer.setContext(map);
493:
494: RenderingHints hints = new RenderingHints(
495: RenderingHints.KEY_ANTIALIASING,
496: RenderingHints.VALUE_ANTIALIAS_ON);
497: renderer.setJava2DHints(hints);
498:
499: // we already do everything that the optimized data loading does...
500: // if we set it to true then it does it all twice...
501: Map rendererParams = new HashMap();
502: rendererParams.put("optimizedDataLoadingEnabled",
503: Boolean.TRUE);
504: rendererParams.put("renderingBuffer", new Integer(
505: mapContext.getBuffer()));
506: renderer.setRendererHints(rendererParams);
507:
508: Envelope dataArea = map.getAreaOfInterest();
509: AffineTransform at = RendererUtilities
510: .worldToScreenTransform(dataArea, paintArea);
511: renderer.paint(graphic, paintArea, dataArea, at);
512: graphic.dispose();
513:
514: // /////////////////////////////////////////////////////////////////
515: //
516: // Storing Image ...
517: //
518: // /////////////////////////////////////////////////////////////////
519: final ZipEntry e = new ZipEntry("layer_" + (i) + ".png");
520: outZ.putNextEntry(e);
521: new ImageWorker(curImage).writePNG(outZ, "FILTERED", 0.75f,
522: false, false);
523: //final MemoryCacheImageOutputStream memOutStream = new MemoryCacheImageOutputStream(outZ);
524: /*final PlanarImage encodedImage = PlanarImage
525: .wrapRenderedImage(curImage);
526: //final PlanarImage finalImage = encodedImage.getColorModel() instanceof DirectColorModel?ImageUtilities
527: // .reformatColorModel2ComponentColorModel(encodedImage):encodedImage;
528: final PlanarImage finalImage = encodedImage;
529: final Iterator it = ImageIO.getImageWritersByMIMEType("image/png");
530: ImageWriter imgWriter = null;
531: if (!it.hasNext()) {
532: LOGGER.warning("No PNG ImageWriter found");
533: throw new IllegalStateException("No PNG ImageWriter found");
534: } else
535: imgWriter = (ImageWriter) it.next();
536: */
537:
538: //---------------------- bo- new
539: // PngEncoderB png = new PngEncoderB(curImage, PngEncoder.ENCODE_ALPHA, 0, 1);
540: // byte[] pngbytes = png.pngEncode();
541: // memOutStream.write(pngbytes);
542: //----------------------
543: //imgWriter.setOutput(memOutStream);
544: //imgWriter.write(null, new IIOImage(finalImage, null, null), null);
545: //memOutStream.flush();
546: //memOutStream.close();
547: //imgWriter.dispose();
548: outZ.closeEntry();
549: }
550: }
551:
552: /**
553: * Creates the bounding box filters (one for each geometric attribute) needed to query a
554: * <code>MapLayer</code>'s feature source to return just the features for the target
555: * rendering extent
556: *
557: * @param schema the layer's feature source schema
558: * @param attributes set of needed attributes
559: * @param bbox the rendering bounding box
560: * @return an or'ed list of bbox filters, one for each geometric attribute in
561: * <code>attributes</code>. If there are just one geometric attribute, just returns
562: * its corresponding <code>GeometryFilter</code>.
563: * @throws IllegalFilterException if something goes wrong creating the filter
564: */
565: private Filter createBBoxFilters(FeatureType schema,
566: String[] attributes, Envelope bbox)
567: throws IllegalFilterException {
568: List filters = new ArrayList();
569: final int length = attributes.length;
570: for (int j = 0; j < length; j++) {
571: AttributeType attType = schema
572: .getAttributeType(attributes[j]);
573:
574: //DJB: added this for better error messages!
575: if (attType == null) {
576: if (LOGGER.isLoggable(Level.FINE)) {
577: LOGGER.fine(new StringBuffer("Could not find '")
578: .append(attributes[j]).append(
579: "' in the FeatureType (").append(
580: schema.getTypeName()).append(")")
581: .toString());
582: }
583:
584: throw new IllegalFilterException(new StringBuffer(
585: "Could not find '").append(
586: attributes[j] + "' in the FeatureType (")
587: .append(schema.getTypeName()).append(")")
588: .toString());
589: }
590:
591: if (attType instanceof GeometryAttributeType) {
592: Filter gfilter = filterFactory.bbox(attType
593: .getLocalName(), bbox.getMinX(),
594: bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY(),
595: null);
596: filters.add(gfilter);
597: }
598: }
599:
600: if (filters.size() == 0)
601: return Filter.INCLUDE;
602: else if (filters.size() == 1)
603: return (Filter) filters.get(0);
604: else
605: return filterFactory.or(filters);
606: }
607: }
|