/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package org.geoserver.kml;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.wms.GetMapRequest;
import org.geoserver.wms.MapLayerInfo;
import org.geoserver.wms.WMS;
import org.geoserver.wms.WMSMapContent;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.map.Layer;
import org.geotools.renderer.lite.RendererUtilities;
import org.geotools.xml.transform.TransformerBase;
import org.geotools.xml.transform.Translator;
import org.xml.sax.ContentHandler;
/**
* Transformer to create a KML document from a {@link WMSMapContent}.
*
* @author Justin Deoliveira
*
* @version $Id$
* @see KMLVectorTransformer
* @see KMLRasterTransformer
*/
public class KMLTransformer extends TransformerBase {
/**
* logger
*/
static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geoserver.kml");
/**
* Flag controlling wether kmz was requested.
*/
boolean kmz = false;
private WMS wms;
public KMLTransformer(WMS wms) {
this.wms = wms;
setNamespaceDeclarationEnabled(false);
}
public Translator createTranslator(ContentHandler handler) {
return new KMLTranslator(handler);
}
public void setKmz(boolean kmz) {
this.kmz = kmz;
}
protected class KMLTranslator extends TranslatorSupport {
/**
* Tolerance used to compare doubles for equality
*/
static final double TOLERANCE = 1e-6;
static final int RULES = 0;
static final int ELSE_RULES = 1;
private double scaleDenominator;
public KMLTranslator(ContentHandler handler) {
super(handler, null, null);
}
public void encode(Object o) throws IllegalArgumentException {
final WMSMapContent mapContent = (WMSMapContent) o;
final GetMapRequest request = mapContent.getRequest();
final List<Layer> layers = mapContent.layers();
final KMLLookAt lookAtOpts = new KMLLookAt(request.getFormatOptions());
// start("kml");
start("kml",
KMLUtils.attributes(new String[] { "xmlns", "http://www.opengis.net/kml/2.2",
"xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation",
"http://www.opengis.net/kml/2.2 http://schemas.opengis.net/kml/2.2.0/ogckml22.xsd" }));
// calculate scale denominator
scaleDenominator = 1;
try {
scaleDenominator = RendererUtilities.calculateScale(mapContent.getRenderingArea(),
mapContent.getMapWidth(), mapContent.getMapHeight(), null);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error calculating scale denominator", e);
}
LOGGER.log(Level.FINE, "scale denominator = " + scaleDenominator);
// if we have more than one layer ( or a legend was requested ),
// use the name "GeoServer" to group them
boolean group = (layers.size() > 1) || request.getLegend();
if (group) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < layers.size(); i++) {
sb.append(layers.get(i).getTitle() + ",");
}
sb.setLength(sb.length() - 1);
start("Document");
element("name", sb.toString());
}
// for every layer specified in the request
for (int i = 0; i < layers.size(); i++) {
// layer and info
Layer layer = layers.get(i);
MapLayerInfo layerInfo = mapContent.getRequest().getLayers().get(i);
// was a super overlay requested?
Boolean superoverlay = (Boolean) mapContent.getRequest().getFormatOptions()
.get("superoverlay");
superoverlay = (superoverlay == null ? Boolean.FALSE : superoverlay);
if (superoverlay) {
// encode as super overlay
encodeSuperOverlayLayer(mapContent, layer);
} else {
// figure out which type of layer this is, raster or vector
if (layerInfo.getType() != MapLayerInfo.TYPE_RASTER) {
// vector
encodeVectorLayer(mapContent, layer, lookAtOpts);
} else {
// encode as normal ground overlay
encodeRasterLayer(mapContent, layer, lookAtOpts);
}
}
}
// legend suppoer
if (request.getLegend()) {
// for every layer specified in the request
for (int i = 0; i < layers.size(); i++) {
// layer and info
Layer layer = layers.get(i);
encodeLegend(mapContent, layer);
}
}
if (group) {
end("Document");
}
end("kml");
}
/**
* Encodes a vector layer as kml.
*/
protected void encodeVectorLayer(WMSMapContent mapContent, Layer layer,
KMLLookAt lookAtOpts) {
// get the data
SimpleFeatureSource featureSource = (SimpleFeatureSource) layer.getFeatureSource();
SimpleFeatureCollection features = null;
try {
features = KMLUtils.loadFeatureCollection(featureSource, layer, mapContent, wms,
scaleDenominator);
if (features == null) {
// it means no features need to be depicted with this style/scale denominator
return;
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
// was kmz requested?
if (kmz) {
// calculate kmscore to determine if we shoud write as vectors
// or pre-render
int kmscore = wms.getKmScore();
Object kmScoreObj = mapContent.getRequest().getFormatOptions().get("kmscore");
if (kmScoreObj != null) {
kmscore = (Integer) kmScoreObj;
}
boolean useVector = useVectorOutput(kmscore, features.size());
if (useVector) {
// encode
KMLVectorTransformer tx = createVectorTransformer(mapContent, layer, lookAtOpts);
initTransformer(tx);
tx.setScaleDenominator(scaleDenominator);
tx.createTranslator(contentHandler).encode(features);
} else {
KMLRasterTransformer tx = createRasterTransfomer(mapContent, lookAtOpts);
initTransformer(tx);
// set inline to true to have the transformer reference images
// inline in the zip file
tx.setInline(true);
tx.createTranslator(contentHandler).encode(layer);
}
} else {
// kmz not selected, just do straight vector
KMLVectorTransformer tx = createVectorTransformer(mapContent, layer, lookAtOpts);
initTransformer(tx);
tx.setScaleDenominator(scaleDenominator);
tx.createTranslator(contentHandler).encode(features);
}
}
/**
* Factory method, allows subclasses to inject their own version of the raster transfomer
*
* @param mapContent
* @param lookAtOpts
* @return
*/
protected KMLRasterTransformer createRasterTransfomer(WMSMapContent mapContent,
KMLLookAt lookAtOpts) {
return new KMLRasterTransformer(wms, mapContent, lookAtOpts);
}
/**
* Factory method, allows subclasses to inject their own version of the vector transfomer
*
* @param mapContent
* @param lookAtOpts
* @return
*/
protected KMLVectorTransformer createVectorTransformer(WMSMapContent mapContent,
Layer layer, KMLLookAt lookAtOpts) {
return new KMLVectorTransformer(wms, mapContent, layer, lookAtOpts);
}
/**
* Encodes a raster layer as kml.
*/
protected void encodeRasterLayer(WMSMapContent mapContent, Layer layer,
KMLLookAt lookAtOpts) {
KMLRasterTransformer tx = createRasterTransfomer(mapContent, lookAtOpts);
initTransformer(tx);
tx.setInline(kmz);
tx.createTranslator(contentHandler).encode(layer);
}
/**
* Encodes a layer as a super overlay.
*/
protected void encodeSuperOverlayLayer(WMSMapContent mapContent, Layer layer) {
KMLSuperOverlayTransformer tx = new KMLSuperOverlayTransformer(wms, mapContent);
initTransformer(tx);
tx.createTranslator(contentHandler).encode(layer);
}
/**
* Encodes the legend for a maper layer as a scree overlay.
*/
protected void encodeLegend(WMSMapContent mapContent, Layer layer) {
KMLLegendTransformer tx = new KMLLegendTransformer(mapContent);
initTransformer(tx);
tx.createTranslator(contentHandler).encode(layer);
}
protected void initTransformer(KMLTransformerBase delegate) {
delegate.setIndentation(getIndentation());
delegate.setEncoding(getEncoding());
delegate.setStandAlone(false);
}
double computeScaleDenominator(Layer layer, WMSMapContent mapContent) {
Rectangle paintArea = new Rectangle(mapContent.getMapWidth(), mapContent.getMapHeight());
AffineTransform worldToScreen = RendererUtilities.worldToScreenTransform(
mapContent.getRenderingArea(), paintArea);
try {
// 90 = OGC standard DPI (see SLD spec page 37)
return RendererUtilities.calculateScale(mapContent.getRenderingArea(),
mapContent.getCoordinateReferenceSystem(), paintArea.width,
paintArea.height, 90);
} catch (Exception e) {
// probably either (1) no CRS (2) error xforming, revert to
// old method - the best we can do (DJB)
return 1 / worldToScreen.getScaleX();
}
}
/**
* Determines whether to return a vector (KML) result of the data or to return an image
* instead. If the kmscore is 100, then the output should always be vector. If the kmscore
* is 0, it should always be raster. In between, the number of features is weighed against
* the kmscore value. kmscore determines whether to return the features as vectors, or as
* one raster image. It is the point, determined by the user, where X number of features is
* "too many" and the result should be returned as an image instead.
*
* kmscore is logarithmic. The higher the value, the more features it takes to make the
* algorithm return an image. The lower the kmscore, the fewer features it takes to force an
* image to be returned. (in use, the formula is exponential: as you increase the KMScore
* value, the number of features required increases exponentially).
*
* @param kmscore
* the score, between 0 and 100, use to determine what output to use
* @param numFeatures
* how many features are being rendered
* @return true: use just kml vectors, false: use raster result
*/
boolean useVectorOutput(int kmscore, int numFeatures) {
if (kmscore == 100) {
return true; // vector KML
}
if (kmscore == 0) {
return false; // raster KMZ
}
// For numbers in between, determine exponentionally based on kmscore value:
// 10^(kmscore/15)
// This results in exponential growth.
// The lowest bound is 1 feature and the highest bound is 3.98 million features
// The most useful kmscore values are between 20 and 70 (21 and 46000 features
// respectively)
// A good default kmscore value is around 40 (464 features)
double magic = Math.pow(10, kmscore / 15);
if (numFeatures > magic) {
return false; // return raster
} else {
return true; // return vector
}
}
}
}