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.data.crs.ReprojectFeatureResults;
013: import org.geotools.factory.CommonFactoryFinder;
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.BBoxExpression;
019: import org.geotools.filter.IllegalFilterException;
020: import org.geotools.geometry.jts.ReferencedEnvelope;
021: import org.geotools.map.MapLayer;
022: import org.geotools.referencing.CRS;
023: import org.geotools.renderer.lite.LiteFeatureTypeStyle;
024: import org.geotools.renderer.lite.RendererUtilities;
025: import org.geotools.styling.FeatureTypeStyle;
026: import org.geotools.styling.Rule;
027: import org.geotools.styling.Style;
028: import org.geotools.xml.transform.TransformerBase;
029: import org.geotools.xml.transform.Translator;
030: import org.opengis.filter.Filter;
031: import org.opengis.filter.FilterFactory;
032: import org.opengis.referencing.crs.CoordinateReferenceSystem;
033: import org.vfny.geoserver.global.MapLayerInfo;
034: import org.vfny.geoserver.wms.WMSMapContext;
035: import org.vfny.geoserver.wms.requests.GetMapRequest;
036: import org.xml.sax.ContentHandler;
037: import java.awt.Rectangle;
038: import java.awt.Transparency;
039: import java.awt.geom.AffineTransform;
040: import java.awt.image.BufferedImage;
041: import java.text.NumberFormat;
042: import java.util.ArrayList;
043: import java.util.Iterator;
044: import java.util.List;
045: import java.util.logging.Level;
046: import java.util.logging.Logger;
047:
048: import javax.xml.transform.Transformer;
049:
050: public class KMLTransformer extends TransformerBase {
051: /**
052: * logger
053: */
054: static Logger LOGGER = org.geotools.util.logging.Logging
055: .getLogger("org.geoserver.kml");
056:
057: /**
058: * Factory used to create filter objects
059: */
060: FilterFactory filterFactory = (FilterFactory) CommonFactoryFinder
061: .getFilterFactory(null);
062:
063: private static final CoordinateReferenceSystem WGS84;
064:
065: static {
066: try {
067: WGS84 = CRS.decode("EPSG:4326");
068: } catch (Exception e) {
069: throw new RuntimeException(
070: "Cannot decode EPSG:4326, the CRS subsystem must be badly broken...");
071: }
072: }
073:
074: /**
075: * Flag controlling wether kmz was requested.
076: */
077: boolean kmz = false;
078:
079: public KMLTransformer() {
080: setNamespaceDeclarationEnabled(false);
081: }
082:
083: public Translator createTranslator(ContentHandler handler) {
084: return new KMLTranslator(handler);
085: }
086:
087: public void setFilterFactory(FilterFactory filterFactory) {
088: this .filterFactory = filterFactory;
089: }
090:
091: public void setKmz(boolean kmz) {
092: this .kmz = kmz;
093: }
094:
095: protected class KMLTranslator extends TranslatorSupport {
096: /**
097: * Tolerance used to compare doubles for equality
098: */
099: static final double TOLERANCE = 1e-6;
100:
101: static final int RULES = 0;
102: static final int ELSE_RULES = 1;
103:
104: private double scaleDenominator;
105:
106: public KMLTranslator(ContentHandler handler) {
107: super (handler, null, null);
108: }
109:
110: public void encode(Object o) throws IllegalArgumentException {
111: start("kml");
112:
113: WMSMapContext mapContext = (WMSMapContext) o;
114: GetMapRequest request = mapContext.getRequest();
115: MapLayer[] layers = mapContext.getLayers();
116:
117: //calculate scale denominator
118: scaleDenominator = 1;
119: try {
120: scaleDenominator = RendererUtilities.calculateScale(
121: mapContext.getAreaOfInterest(), mapContext
122: .getMapWidth(), mapContext
123: .getMapHeight(), null);
124: } catch (Exception e) {
125: LOGGER.log(Level.WARNING,
126: "Error calculating scale denominator", e);
127: }
128: LOGGER.fine("scale denominator = " + scaleDenominator);
129:
130: //if we have more than one layer ( or a legend was requested ),
131: //use the name "GeoServer" to group them
132: boolean group = (layers.length > 1) || request.getLegend();
133:
134: if (group) {
135: StringBuffer sb = new StringBuffer();
136: for (int i = 0; i < layers.length; i++) {
137: sb.append(layers[i].getTitle() + ",");
138: }
139: sb.setLength(sb.length() - 1);
140:
141: start("Document");
142: element("name", sb.toString());
143: }
144:
145: //for every layer specified in the request
146: for (int i = 0; i < layers.length; i++) {
147: //layer and info
148: MapLayer layer = layers[i];
149: MapLayerInfo layerInfo = mapContext.getRequest()
150: .getLayers()[i];
151:
152: //was a super overlay requested?
153: if (mapContext.getRequest().getSuperOverlay()) {
154: //encode as super overlay
155: encodeSuperOverlayLayer(mapContext, layer);
156: } else {
157: //figure out which type of layer this is, raster or vector
158: if (layerInfo.getType() == MapLayerInfo.TYPE_VECTOR
159: || layerInfo.getType() == MapLayerInfo.TYPE_REMOTE_VECTOR) {
160: //vector
161: encodeVectorLayer(mapContext, layer);
162: } else {
163: //encode as normal ground overlay
164: encodeRasterLayer(mapContext, layer);
165: }
166: }
167: }
168:
169: //legend suppoer
170: if (request.getLegend()) {
171: //for every layer specified in the request
172: for (int i = 0; i < layers.length; i++) {
173: //layer and info
174: MapLayer layer = layers[i];
175: encodeLegend(mapContext, layer);
176: }
177: }
178:
179: if (group) {
180: end("Document");
181: }
182:
183: end("kml");
184: }
185:
186: /**
187: * Encodes a vector layer as kml.
188: */
189: protected void encodeVectorLayer(WMSMapContext mapContext,
190: MapLayer layer) {
191: //get the data
192: FeatureSource featureSource = layer.getFeatureSource();
193: FeatureCollection features = null;
194:
195: try {
196: features = loadFeatureCollection(featureSource, layer,
197: mapContext);
198: if (features == null)
199: return;
200: } catch (Exception e) {
201: throw new RuntimeException(e);
202: }
203:
204: //was kmz requested?
205: if (kmz) {
206: //calculate kmscore to determine if we shoud write as vectors
207: // or pre-render
208: int kmscore = mapContext.getRequest().getKMScore();
209: boolean useVector = useVectorOutput(kmscore, features
210: .size());
211:
212: if (useVector) {
213: //encode
214: KMLVectorTransformer tx = createVectorTransformer(
215: mapContext, layer);
216: initTransformer(tx);
217: tx.setScaleDenominator(scaleDenominator);
218: tx.createTranslator(contentHandler)
219: .encode(features);
220: } else {
221: KMLRasterTransformer tx = createRasterTransfomer(mapContext);
222: initTransformer(tx);
223:
224: //set inline to true to have the transformer reference images
225: // inline in the zip file
226: tx.setInline(true);
227: tx.createTranslator(contentHandler).encode(layer);
228: }
229: } else {
230: //kmz not selected, just do straight vector
231: KMLVectorTransformer tx = createVectorTransformer(
232: mapContext, layer);
233: initTransformer(tx);
234: tx.setScaleDenominator(scaleDenominator);
235: tx.createTranslator(contentHandler).encode(features);
236: }
237: }
238:
239: /**
240: * Factory method, allows subclasses to inject their own version of the raster transfomer
241: * @param mapContext
242: * @return
243: */
244: protected KMLRasterTransformer createRasterTransfomer(
245: WMSMapContext mapContext) {
246: return new KMLRasterTransformer(mapContext);
247: }
248:
249: /**
250: * Factory method, allows subclasses to inject their own version of the vector transfomer
251: * @param mapContext
252: * @return
253: */
254: protected KMLVectorTransformer createVectorTransformer(
255: WMSMapContext mapContext, MapLayer layer) {
256: return new KMLVectorTransformer(mapContext, layer);
257: }
258:
259: /**
260: * Encodes a raster layer as kml.
261: */
262: protected void encodeRasterLayer(WMSMapContext mapContext,
263: MapLayer layer) {
264: KMLRasterTransformer tx = createRasterTransfomer(mapContext);
265: initTransformer(tx);
266:
267: tx.setInline(kmz);
268: tx.createTranslator(contentHandler).encode(layer);
269: }
270:
271: /**
272: * Encodes a layer as a super overlay.
273: */
274: protected void encodeSuperOverlayLayer(
275: WMSMapContext mapContext, MapLayer layer) {
276: KMLSuperOverlayTransformer tx = new KMLSuperOverlayTransformer(
277: mapContext);
278: initTransformer(tx);
279: tx.createTranslator(contentHandler).encode(layer);
280: }
281:
282: /**
283: * Encodes the legend for a maper layer as a scree overlay.
284: */
285: protected void encodeLegend(WMSMapContext mapContext,
286: MapLayer layer) {
287: KMLLegendTransformer tx = new KMLLegendTransformer(
288: mapContext);
289: initTransformer(tx);
290: tx.createTranslator(contentHandler).encode(layer);
291: }
292:
293: protected void initTransformer(KMLTransformerBase delegate) {
294: delegate.setIndentation(getIndentation());
295: delegate.setStandAlone(false);
296: }
297:
298: double computeScaleDenominator(MapLayer layer,
299: WMSMapContext mapContext) {
300: Rectangle paintArea = new Rectangle(mapContext
301: .getMapWidth(), mapContext.getMapHeight());
302: AffineTransform worldToScreen = RendererUtilities
303: .worldToScreenTransform(mapContext
304: .getAreaOfInterest(), paintArea);
305:
306: try {
307: //90 = OGC standard DPI (see SLD spec page 37)
308: return RendererUtilities.calculateScale(mapContext
309: .getAreaOfInterest(), mapContext
310: .getCoordinateReferenceSystem(),
311: paintArea.width, paintArea.height, 90);
312: } catch (Exception e) {
313: //probably either (1) no CRS (2) error xforming, revert to
314: // old method - the best we can do (DJB)
315: return 1 / worldToScreen.getScaleX();
316: }
317: }
318:
319: /**
320: * Determines whether to return a vector (KML) result of the data or to
321: * return an image instead.
322: * If the kmscore is 100, then the output should always be vector. If
323: * the kmscore is 0, it should always be raster. In between, the number of
324: * features is weighed against the kmscore value.
325: * kmscore determines whether to return the features as vectors, or as one
326: * raster image. It is the point, determined by the user, where X number of
327: * features is "too many" and the result should be returned as an image instead.
328: *
329: * kmscore is logarithmic. The higher the value, the more features it takes
330: * to make the algorithm return an image. The lower the kmscore, the fewer
331: * features it takes to force an image to be returned.
332: * (in use, the formula is exponential: as you increase the KMScore value,
333: * the number of features required increases exponentially).
334: *
335: * @param kmscore the score, between 0 and 100, use to determine what output to use
336: * @param numFeatures how many features are being rendered
337: * @return true: use just kml vectors, false: use raster result
338: */
339: boolean useVectorOutput(int kmscore, int numFeatures) {
340: if (kmscore == 100) {
341: return true; // vector KML
342: }
343:
344: if (kmscore == 0) {
345: return false; // raster KMZ
346: }
347:
348: // For numbers in between, determine exponentionally based on kmscore value:
349: // 10^(kmscore/15)
350: // This results in exponential growth.
351: // The lowest bound is 1 feature and the highest bound is 3.98 million features
352: // The most useful kmscore values are between 20 and 70 (21 and 46000 features respectively)
353: // A good default kmscore value is around 40 (464 features)
354: double magic = Math.pow(10, kmscore / 15);
355:
356: if (numFeatures > magic) {
357: return false; // return raster
358: } else {
359: return true; // return vector
360: }
361: }
362:
363: FeatureCollection loadFeatureCollection(
364: FeatureSource featureSource, MapLayer layer,
365: WMSMapContext mapContext) throws Exception {
366: FeatureType schema = featureSource.getSchema();
367:
368: Envelope envelope = mapContext.getAreaOfInterest();
369: ReferencedEnvelope aoi = new ReferencedEnvelope(envelope,
370: mapContext.getCoordinateReferenceSystem());
371: CoordinateReferenceSystem sourceCrs = schema
372: .getDefaultGeometry().getCoordinateSystem();
373:
374: boolean reprojectBBox = (sourceCrs != null)
375: && !CRS.equalsIgnoreMetadata(aoi
376: .getCoordinateReferenceSystem(), sourceCrs);
377: if (reprojectBBox) {
378: aoi = aoi.transform(sourceCrs, true);
379: }
380:
381: Filter filter = createBBoxFilter(schema, aoi);
382:
383: // now build the query using only the attributes and the bounding
384: // box needed
385: DefaultQuery q = new DefaultQuery(schema.getTypeName());
386: q.setFilter(filter);
387:
388: // now, if a definition query has been established for this layer, be
389: // sure to respect it by combining it with the bounding box one.
390: Query definitionQuery = layer.getQuery();
391:
392: if (definitionQuery != Query.ALL) {
393: if (q == Query.ALL) {
394: q = (DefaultQuery) definitionQuery;
395: } else {
396: q = (DefaultQuery) DataUtilities.mixQueries(
397: definitionQuery, q, "KMLEncoder");
398: }
399: }
400:
401: // make sure we output in 4326 since that's what KML mandates
402: if (sourceCrs != null
403: && !CRS.equalsIgnoreMetadata(WGS84, sourceCrs)) {
404: return new ReprojectFeatureResults(featureSource
405: .getFeatures(q), WGS84);
406: }
407:
408: // extract the actual rules that are going to be applied to this layer,
409: // - if none applies (scale denominator rules) then nothing to render
410: // - if there are else rules, we have to load everything
411: // - if there are no else rules, we can try to limit the features read by summarizing
412: // the direct filter rules
413: List[] rules = getLayerRules(featureSource.getSchema(),
414: layer.getStyle());
415: if (rules[RULES].size() == 0
416: && rules[ELSE_RULES].size() == 0)
417: return null;
418: if (rules[ELSE_RULES].size() == 0) {
419: Filter newFilter = summarizeRuleFilters(rules[RULES], q
420: .getFilter());
421: q.setFilter(newFilter);
422: }
423:
424: return featureSource.getFeatures(q);
425: }
426:
427: private List[] getLayerRules(FeatureType ftype, Style style) {
428: List[] result = new List[] { new ArrayList(),
429: new ArrayList() };
430:
431: final String typeName = ftype.getTypeName();
432: FeatureTypeStyle[] featureStyles = style
433: .getFeatureTypeStyles();
434: final int length = featureStyles.length;
435: for (int i = 0; i < length; i++) {
436: // getting feature styles
437: FeatureTypeStyle fts = featureStyles[i];
438:
439: // check if this FTS is compatible with this FT.
440: if ((typeName != null)
441: && (ftype.isDescendedFrom(null, fts
442: .getFeatureTypeName()) || typeName
443: .equalsIgnoreCase(fts
444: .getFeatureTypeName()))) {
445:
446: // get applicable rules at the current scale
447: Rule[] ftsRules = fts.getRules();
448: for (int j = 0; j < ftsRules.length; j++) {
449: // getting rule
450: Rule r = ftsRules[j];
451:
452: if (isWithInScale(r)) {
453: if (r.hasElseFilter()) {
454: result[ELSE_RULES].add(r);
455: } else {
456: result[RULES].add(r);
457: }
458: }
459: }
460: }
461: }
462:
463: return result;
464: }
465:
466: /**
467: * Tries to build a more restrictive filter by creating a summary of filters that
468: * apply to each rule (or returns simply the original filter, if there is at least one
469: * rule that applies to all features)
470: * @param rules
471: * @param originalFiter
472: * @return
473: */
474: private Filter summarizeRuleFilters(List rules,
475: Filter originalFiter) {
476: List filters = new ArrayList();
477: for (Iterator it = rules.iterator(); it.hasNext();) {
478: Rule rule = (Rule) it.next();
479: // if there is a single rule asking for all filters, we have to
480: // return everything that the original filter returned already
481: if (rule.getFilter() == null
482: || Filter.INCLUDE.equals(rule.getFilter()))
483: return originalFiter;
484: else
485: filters.add(rule.getFilter());
486: }
487:
488: org.opengis.filter.FilterFactory ff = CommonFactoryFinder
489: .getFilterFactory(null);
490: Filter summary = ff.or(filters);
491: if (originalFiter != null
492: && !Filter.INCLUDE.equals(originalFiter))
493: return ff.and(originalFiter, summary);
494: else
495: return summary;
496: }
497:
498: /**
499: * Checks if a rule can be triggered at the current scale level
500: *
501: * @param r
502: * The rule
503: * @return true if the scale is compatible with the rule settings
504: */
505: boolean isWithInScale(Rule r) {
506: return ((r.getMinScaleDenominator() - TOLERANCE) <= scaleDenominator)
507: && ((r.getMaxScaleDenominator() + TOLERANCE) > scaleDenominator);
508: }
509:
510: /** Creates the bounding box filters (one for each geometric attribute) needed to query a
511: * <code>MapLayer</code>'s feature source to return just the features for the target
512: * rendering extent
513: *
514: * @param schema the layer's feature source schema
515: * @param bbox the expression holding the target rendering bounding box
516: * @return an or'ed list of bbox filters, one for each geometric attribute in
517: * <code>attributes</code>. If there are just one geometric attribute, just returns
518: * its corresponding <code>GeometryFilter</code>.
519: * @throws IllegalFilterException if something goes wrong creating the filter
520: */
521: Filter createBBoxFilter(FeatureType schema, Envelope bbox)
522: throws IllegalFilterException {
523: List filters = new ArrayList();
524: for (int j = 0; j < schema.getAttributeCount(); j++) {
525: AttributeType attType = schema.getAttributeType(j);
526:
527: if (attType instanceof GeometryAttributeType) {
528: Filter gfilter = filterFactory.bbox(attType
529: .getLocalName(), bbox.getMinX(), bbox
530: .getMinY(), bbox.getMaxX(), bbox.getMaxY(),
531: null);
532: filters.add(gfilter);
533: }
534: }
535:
536: if (filters.size() == 0)
537: return Filter.INCLUDE;
538: else if (filters.size() == 1)
539: return (Filter) filters.get(0);
540: else
541: return filterFactory.or(filters);
542: }
543: }
544: }
|