package org.geoserver.kml;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.wms.GetMapRequest;
import org.geoserver.wms.MapLayerInfo;
import org.geoserver.wms.WMS;
import org.geoserver.wms.WMSMapContent;
import org.geoserver.wms.WMSRequests;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.feature.FeatureCollection;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.Layer;
import org.geotools.referencing.CRS;
import org.geotools.styling.Style;
import org.geotools.xml.transform.TransformerBase;
import org.geotools.xml.transform.Translator;
import org.opengis.filter.Filter;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.xml.sax.ContentHandler;
import com.vividsolutions.jts.geom.Envelope;
/**
* Encodes a KML document contianing a network link.
* <p>
* This transformer transforms a {@link GetMapRequest} object.
* </p>
*
* @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org
*
*/
public class KMLNetworkLinkTransformer extends TransformerBase {
/**
* logger
*/
static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geoserver.kml");
/**
* flag controlling whether the network link should be a super overlay.
*/
boolean encodeAsRegion = false;
/**
* flag controlling whether the network link should be a direct GWC one when possible
*/
boolean cachedMode = false;
private boolean standalone;
/**
* @see #setInline
*/
private boolean inline;
private WMS wms;
public KMLNetworkLinkTransformer(WMS wms) {
this.wms = wms;
standalone = true;
}
public void setStandalone(boolean standalone){
this.standalone = standalone;
}
public boolean isStandalone(){
return standalone;
}
/**
* @return {@code true} if the document is to be generated inline (i.e. without an enclosing
* Folder element). Defaults to {@code false}
*/
public boolean isInline() {
return inline;
}
/**
* @param inline if {@code true} network links won't be enclosed inside a Folder element
*/
public void setInline(boolean inline) {
this.inline = inline;
}
public void setCachedMode(boolean cachedMode) {
this.cachedMode = cachedMode;
}
public Translator createTranslator(ContentHandler handler) {
return new KMLNetworkLinkTranslator(handler);
}
public void setEncodeAsRegion(boolean encodeAsRegion) {
this.encodeAsRegion = encodeAsRegion;
}
class KMLNetworkLinkTranslator extends TranslatorSupport {
public KMLNetworkLinkTranslator(ContentHandler contentHandler) {
super(contentHandler, null, null);
}
public void encode(Object o) throws IllegalArgumentException {
final WMSMapContent context = (WMSMapContent) o;
final GetMapRequest request = context.getRequest();
// restore target mime type for the network links
if (NetworkLinkMapOutputFormat.KML_MIME_TYPE.equals(request.getFormat())) {
request.setFormat(KMLMapOutputFormat.MIME_TYPE);
} else {
request.setFormat(KMZMapOutputFormat.MIME_TYPE);
}
if(standalone){
start("kml");
}
if (!inline) {
start("Folder");
}
final List<MapLayerInfo> layers = request.getLayers();
final KMLLookAt lookAt = parseLookAtOptions(request);
ReferencedEnvelope aggregatedBounds;
List<ReferencedEnvelope> layerBounds;
layerBounds = new ArrayList<ReferencedEnvelope>(layers.size());
aggregatedBounds = computePerLayerQueryBounds(context, layerBounds, lookAt);
if (encodeAsRegion) {
encodeAsSuperOverlay(request, lookAt, layerBounds);
} else {
encodeAsOverlay(request, lookAt, layerBounds);
}
// look at
encodeLookAt(aggregatedBounds, lookAt);
if (!inline) {
end("Folder");
}
if (standalone) {
end("kml");
}
}
/**
* @return the aggregated bounds for all the requested layers, taking into account whether
* the whole layer or filtered bounds is used for each layer
*/
private ReferencedEnvelope computePerLayerQueryBounds(final WMSMapContent context,
final List<ReferencedEnvelope> target, final KMLLookAt lookAt) {
// no need to compute queried bounds if request explicitly specified the view area
final boolean computeQueryBounds = lookAt.getLookAt() == null;
ReferencedEnvelope aggregatedBounds;
try {
boolean longitudeFirst = true;
aggregatedBounds = new ReferencedEnvelope(CRS.decode("EPSG:4326", longitudeFirst));
} catch (Exception e) {
throw new RuntimeException(e);
}
aggregatedBounds.setToNull();
final List<Layer> mapLayers = context.layers();
final List<MapLayerInfo> layerInfos = context.getRequest().getLayers();
for (int i = 0; i < mapLayers.size(); i++) {
final Layer Layer = mapLayers.get(i);
final MapLayerInfo layerInfo = layerInfos.get(i);
ReferencedEnvelope layerLatLongBbox;
layerLatLongBbox = computeLayerBounds(Layer, layerInfo, computeQueryBounds);
try {
layerLatLongBbox = layerLatLongBbox.transform(aggregatedBounds.getCoordinateReferenceSystem(), true);
} catch (Exception e) {
throw new RuntimeException(e);
}
target.add(layerLatLongBbox);
aggregatedBounds.expandToInclude(layerLatLongBbox);
}
return aggregatedBounds;
}
@SuppressWarnings("rawtypes")
private ReferencedEnvelope computeLayerBounds(Layer layer, MapLayerInfo layerInfo,
boolean computeQueryBounds) {
final Query layerQuery = layer.getQuery();
// make sure if layer is gonna be filtered, the resulting bounds are obtained instead of
// the whole bounds
final Filter filter = layerQuery.getFilter();
if (layerQuery.getFilter() == null || Filter.INCLUDE.equals(filter)) {
computeQueryBounds = false;
}
if (!computeQueryBounds && !layerQuery.isMaxFeaturesUnlimited()) {
computeQueryBounds = true;
}
ReferencedEnvelope layerLatLongBbox = null;
if (computeQueryBounds) {
FeatureSource featureSource = layer.getFeatureSource();
try {
CoordinateReferenceSystem targetCRS = CRS.decode("EPSG:4326");
FeatureCollection features = featureSource.getFeatures(layerQuery);
layerLatLongBbox = features.getBounds();
layerLatLongBbox = layerLatLongBbox.transform(targetCRS, true);
} catch (Exception e) {
LOGGER.info("Error computing bounds for " + featureSource.getName() + " with "
+ layerQuery);
}
}
if (layerLatLongBbox == null) {
try {
layerLatLongBbox = layerInfo.getLatLongBoundingBox();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return layerLatLongBbox;
}
@SuppressWarnings("unchecked")
private KMLLookAt parseLookAtOptions(final GetMapRequest request) {
final KMLLookAt lookAt;
if (request.getFormatOptions() == null) {
// use a default LookAt properties
lookAt = new KMLLookAt();
} else {
// use the requested LookAt properties
Map<String, Object> formatOptions;
formatOptions = new HashMap<String, Object>(request.getFormatOptions());
lookAt = new KMLLookAt(formatOptions);
/*
* remove LOOKATBBOX and LOOKATGEOM from format options so KMLUtils.getMapRequest
* does not include them in the network links, but do include the other options such
* as tilt, range, etc.
*/
request.getFormatOptions().remove("LOOKATBBOX");
request.getFormatOptions().remove("LOOKATGEOM");
}
return lookAt;
}
protected void encodeAsSuperOverlay(GetMapRequest request, KMLLookAt lookAt,
List<ReferencedEnvelope> layerBounds) {
List<MapLayerInfo> layers = request.getLayers();
List<Style> styles = request.getStyles();
for (int i = 0; i < layers.size(); i++) {
MapLayerInfo layer = layers.get(i);
if ("cached".equals(KMLUtils.getSuperoverlayMode(request, wms))
&& KMLUtils.isRequestGWCCompatible(request, i, wms)) {
encodeGWCLink(request, layer);
} else {
String styleName = i < styles.size() ? styles.get(i).getName() : null;
ReferencedEnvelope bounds = layerBounds.get(i);
encodeLayerSuperOverlay(request, layer, styleName, i, bounds, lookAt);
}
}
}
public void encodeGWCLink(GetMapRequest request, MapLayerInfo layer) {
start("NetworkLink");
String prefixedName = layer.getResource().getPrefixedName();
element("name", "GWC-" + prefixedName);
start("Link");
String type = layer.getType() == MapLayerInfo.TYPE_RASTER ? "png" : "kml";
String url = ResponseUtils.buildURL(request.getBaseUrl(), "gwc/service/kml/" +
prefixedName + "." + type + ".kml", null, URLType.SERVICE);
element("href", url);
element("viewRefreshMode", "never");
end("Link");
end("NetworkLink");
}
private void encodeLayerSuperOverlay(GetMapRequest request, MapLayerInfo layer,
String styleName, int layerIndex, ReferencedEnvelope bounds, KMLLookAt lookAt) {
start("NetworkLink");
element("name", layer.getName());
element("open", "1");
element("visibility", "1");
// look at for the network link for this single layer
if (bounds != null) {
encodeLookAt(bounds, lookAt);
}
// region
start("Region");
Envelope bbox = request.getBbox();
start("LatLonAltBox");
element("north", "" + bbox.getMaxY());
element("south", "" + bbox.getMinY());
element("east", "" + bbox.getMaxX());
element("west", "" + bbox.getMinX());
end("LatLonAltBox");
start("Lod");
element("minLodPixels", "128");
element("maxLodPixels", "-1");
end("Lod");
end("Region");
// link
start("Link");
String href = WMSRequests.getGetMapUrl(request, layer.getName(), layerIndex, styleName,
null, null);
try {
// WMSRequests.getGetMapUrl returns a URL encoded query string, but GoogleEarth
// 6 doesn't like URL encoded parameters. See GEOS-4483
href = URLDecoder.decode(href, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
start("href");
cdata(href);
end("href");
// element( "viewRefreshMode", "onRegion" );
end("Link");
end("NetworkLink");
}
protected void encodeAsOverlay(GetMapRequest request, KMLLookAt lookAt,
List<ReferencedEnvelope> layerBounds) {
final List<MapLayerInfo> layers = request.getLayers();
final List<Style> styles = request.getStyles();
for (int i = 0; i < layers.size(); i++) {
MapLayerInfo layerInfo = layers.get(i);
start("NetworkLink");
element("name", layerInfo.getName());
element("visibility", "1");
element("open", "1");
// look at for the network link for this single layer
ReferencedEnvelope latLongBoundingBox = layerBounds.get(i);
if (latLongBoundingBox != null) {
encodeLookAt(latLongBoundingBox, lookAt);
}
start("Url");
// set bbox to null so its not included in the request, google
// earth will append it for us
request.setBbox(null);
String style = i < styles.size() ? styles.get(i).getName() : null;
String href = WMSRequests.getGetMapUrl(request, layers.get(i).getName(), i, style,
null, null);
try {
// WMSRequests.getGetMapUrl returns a URL encoded query string, but GoogleEarth
// 6 doesn't like URL encoded parameters. See GEOS-4483
href = URLDecoder.decode(href, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
start("href");
cdata(href);
end("href");
element("viewRefreshMode", "onStop");
element("viewRefreshTime", "1");
end("Url");
end("NetworkLink");
}
}
private void encodeLookAt(Envelope bounds, KMLLookAt lookAt) {
Envelope lookAtEnvelope = null;
if (lookAt.getLookAt() == null) {
lookAtEnvelope = bounds;
}
KMLLookAtTransformer tr;
tr = new KMLLookAtTransformer(lookAtEnvelope, getIndentation(), getEncoding());
Translator translator = tr.createTranslator(contentHandler);
translator.encode(lookAt);
}
}
}