package org.geoserver.kml.builder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.kml.KMLMapOutputFormat;
import org.geoserver.kml.KmlEncodingContext;
import org.geoserver.kml.NetworkLinkMapOutputFormat;
import org.geoserver.kml.decorator.LookAtDecoratorFactory;
import org.geoserver.kml.regionate.Tile;
import org.geoserver.kml.utils.KMLFeatureAccessor;
import org.geoserver.kml.utils.LookAtOptions;
import org.geoserver.ows.HttpErrorCodeException;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.ows.util.CaseInsensitiveMap;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.ServiceException;
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.geometry.jts.ReferencedEnvelope;
import org.geotools.map.FeatureLayer;
import org.geotools.map.Layer;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.styling.Style;
import org.opengis.filter.Filter;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Envelope;
import de.micromata.opengis.kml.v_2_2_0.AbstractLatLonBox;
import de.micromata.opengis.kml.v_2_2_0.Document;
import de.micromata.opengis.kml.v_2_2_0.Feature;
import de.micromata.opengis.kml.v_2_2_0.Folder;
import de.micromata.opengis.kml.v_2_2_0.GroundOverlay;
import de.micromata.opengis.kml.v_2_2_0.Icon;
import de.micromata.opengis.kml.v_2_2_0.LatLonAltBox;
import de.micromata.opengis.kml.v_2_2_0.LatLonBox;
import de.micromata.opengis.kml.v_2_2_0.Link;
import de.micromata.opengis.kml.v_2_2_0.Lod;
import de.micromata.opengis.kml.v_2_2_0.LookAt;
import de.micromata.opengis.kml.v_2_2_0.NetworkLink;
import de.micromata.opengis.kml.v_2_2_0.Region;
import de.micromata.opengis.kml.v_2_2_0.ViewRefreshMode;
/**
* Builds a KML document with a superoverlay hierarchy for each layer
*
* @author Andrea Aime - GeoSolutions
*
*/
public class SuperOverlayNetworkLinkBuilder extends AbstractNetworkLinkBuilder {
private GetMapRequest request;
private WMSMapContent mapContent;
private WMS wms;
static final String VISIBLE_KEY = "kmlvisible";
public SuperOverlayNetworkLinkBuilder(KmlEncodingContext context) {
super(context);
this.request = context.getRequest();
this.mapContent = context.getMapContent();
this.wms = context.getWms();
}
@Override
void encodeDocumentContents(Document container) {
boolean cachedMode = "cached".equals(context.getSuperOverlayMode());
// normalize the requested bounds to match a WGS84 hierarchy tile
Tile tile = new Tile(new ReferencedEnvelope(request.getBbox(), Tile.WGS84));
while(tile.getZ() > 0 && !tile.getEnvelope().contains(request.getBbox())) {
tile = tile.getParent();
}
Envelope normalizedEnvelope = null;
if (tile.getZ() >= 0 && tile.getEnvelope().contains(request.getBbox())) {
normalizedEnvelope = tile.getEnvelope();
} else {
normalizedEnvelope = KmlEncodingContext.WORLD_BOUNDS_WGS84;
}
int zoomLevel = (int) tile.getZ();
// encode top level region, which is always visible
addRegion(container, normalizedEnvelope, Integer.MAX_VALUE, -1);
List<MapLayerInfo> layers = request.getLayers();
for (int i = 0; i < layers.size(); i++) {
MapLayerInfo layer = layers.get(i);
if (cachedMode && isRequestGWCCompatible(request, i, wms)) {
encodeGWCLink(container, request, layer);
} else {
encodeLayerSuperOverlay(container, layer, i, normalizedEnvelope, zoomLevel);
}
}
}
public void encodeGWCLink(Document container, GetMapRequest request, MapLayerInfo layer) {
NetworkLink nl = container.createAndAddNetworkLink();
String prefixedName = layer.getResource().prefixedName();
nl.setName("GWC-" + prefixedName);
Link link = nl.createAndSetLink();
String type = layer.getType() == MapLayerInfo.TYPE_RASTER ? "png" : "kml";
String url = ResponseUtils.buildURL(request.getBaseUrl(), "gwc/service/kml/" + prefixedName
+ "." + type + ".kml", null, URLType.SERVICE);
link.setHref(url);
link.setViewRefreshMode(ViewRefreshMode.NEVER);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void encodeLayerSuperOverlay(Document container, MapLayerInfo layerInfo, int layerIndex, Envelope bounds,
int zoomLevel) {
Map formatOptions = request.getFormatOptions();
Layer layer = mapContent.layers().get(layerIndex);
Folder folder = container.createAndAddFolder();
folder.setName(layerInfo.getLabel());
if (layerInfo.getDescription() != null && layerInfo.getDescription().length() > 0) {
folder.setDescription(layerInfo.getDescription());
}
// Allow for all layers to be disabled by default. This can be advantageous with
// multiple large data-sets.
if (formatOptions.get(VISIBLE_KEY) != null) {
boolean visible = Boolean.parseBoolean(formatOptions.get(VISIBLE_KEY).toString());
folder.setVisibility(visible);
}
else {
folder.setVisibility(true);
}
LookAtOptions lookAtOptions = new LookAtOptions(formatOptions);
if (bounds != null) {
LookAtDecoratorFactory lookAtFactory = new LookAtDecoratorFactory();
ReferencedEnvelope layerBounds = layer.getBounds();
CoordinateReferenceSystem layerCRS = layerBounds.getCoordinateReferenceSystem();
if(layerCRS != null && !CRS.equalsIgnoreMetadata(layerCRS, DefaultGeographicCRS.WGS84)) {
try {
layerBounds = layerBounds.transform(DefaultGeographicCRS.WGS84, true);
} catch(Exception e) {
throw new ServiceException("Failed to transform the layer bounds for "
+ layer.getTitle() + " to WGS84", e);
}
}
LookAt la = lookAtFactory.buildLookAt(layerBounds, lookAtOptions, false);
folder.setAbstractView(la);
}
encodeNetworkLinks(folder, layer, bounds, zoomLevel);
}
/**
* Encode the network links for the specified envelope and zoom level
*
* @param layer
* @param top
* @param zoomLevel
*/
void encodeNetworkLinks(Folder folder, Layer layer, Envelope top, int zoomLevel) {
// encode the network links
if (top != KmlEncodingContext.WORLD_BOUNDS_WGS84) {
// top left
Envelope e00 = new Envelope(top.getMinX(), top.getMinX() + (top.getWidth() / 2d),
top.getMaxY() - (top.getHeight() / 2d), top.getMaxY());
// top right
Envelope e01 = new Envelope(e00.getMaxX(), top.getMaxX(), e00.getMinY(), e00.getMaxY());
// bottom left
Envelope e10 = new Envelope(e00.getMinX(), e00.getMaxX(), top.getMinY(), e00.getMinY());
// bottom right
Envelope e11 = new Envelope(e01.getMinX(), e01.getMaxX(), e10.getMinY(), e10.getMaxY());
addNetworkLink(folder, e00, "00", layer);
addNetworkLink(folder, e01, "01", layer);
addNetworkLink(folder, e10, "10", layer);
addNetworkLink(folder, e11, "11", layer);
} else {
// divide up horizontally by two
Envelope e0 = new Envelope(top.getMinX(), top.getMinX() + (top.getWidth() / 2d),
top.getMinY(), top.getMaxY());
Envelope e1 = new Envelope(e0.getMaxX(), top.getMaxX(), top.getMinY(), top.getMaxY());
addNetworkLink(folder, e0, "0", layer);
addNetworkLink(folder, e1, "1", layer);
}
// encode the ground overlay(s)
if (top == KmlEncodingContext.WORLD_BOUNDS_WGS84) {
// special case for top since it does not line up as a proper
// tile -> split it in two
encodeTileContents(folder, layer, "contents-0", zoomLevel, new Envelope(-180, 0, -90,
90));
encodeTileContents(folder, layer, "contents-1", zoomLevel,
new Envelope(0, 180, -90, 90));
} else {
// encode straight up
encodeTileContents(folder, layer, "contents", zoomLevel, top);
}
}
void addRegion(Feature container, Envelope box, int minLodPixels, int maxLodPixels) {
Region region = container.createAndSetRegion();
Lod lod = region.createAndSetLod();
lod.setMinLodPixels(minLodPixels);
lod.setMaxLodPixels(maxLodPixels);
LatLonAltBox llaBox = region.createAndSetLatLonAltBox();
setEnvelope(box, llaBox);
}
private void setEnvelope(Envelope box, AbstractLatLonBox llBox) {
llBox.setNorth(box.getMaxY());
llBox.setSouth(box.getMinY());
llBox.setEast(box.getMaxX());
llBox.setWest(box.getMinX());
}
void addNetworkLink(Folder container, Envelope box, String name, Layer layer) {
// check if we are going to get any feature from this layer
String overlayMode = context.getSuperOverlayMode();
if (!"raster".equals(overlayMode) && layer instanceof FeatureLayer && !shouldDrawVectorLayer(layer, box)) {
return;
}
NetworkLink nl = container.createAndAddNetworkLink();
nl.setName(name);
addRegion(nl, box, 128, -1);
Link link = nl.createAndSetLink();
String getMap = WMSRequests.getGetMapUrl(request, layer, 0, box, new String[] { "format",
KMLMapOutputFormat.MIME_TYPE, "width", "256", "height", "256", "format", NetworkLinkMapOutputFormat.KML_MIME_TYPE });
link.setHref(getMap);
LOGGER.fine("Network link " + name + ":" + getMap);
link.setViewRefreshMode(ViewRefreshMode.ON_REGION);
}
void encodeTileContents(Folder container, Layer layer, String name, int drawOrder, Envelope box) {
try {
if (shouldDrawVectorLayer(layer, box))
encodeKMLLink(container, layer, name, drawOrder, box);
if (shouldDrawWMSOverlay(layer, box))
encodeGroundOverlay(container, layer, drawOrder, box);
} catch (HttpErrorCodeException e) {
// no contents, ok
}
}
private boolean shouldDrawVectorLayer(Layer layer, Envelope box) {
try {
// should draw as vector if the layer is a vector layer, and based on mode
// full: yes, if any regionated vectors are present at this zoom level
// hybrid: yes, if any regionated vectors are present at this zoom level
// overview: is the non-regionated feature count for this tile below the cutoff?
// raster: no
if (!isVectorLayer(layer))
return false;
String overlayMode = context.getSuperOverlayMode();
if ("raster".equals(overlayMode))
return false;
if ("overview".equals(overlayMode)) {
int featureCount = featuresInTile(layer, box, false);
return featureCount <= getRegionateFeatureLimit(getFeatureTypeInfo(layer));
}
int featureCount = featuresInTile(layer, box, true);
return featureCount > 0;
} catch (HttpErrorCodeException e) {
// fine, it means there was no data.... sigh...
return false;
}
}
private int getRegionateFeatureLimit(FeatureTypeInfo ft) {
Integer regionateFeatureLimit = ft.getMetadata().get("kml.regionateFeatureLimit",
Integer.class);
return regionateFeatureLimit != null ? regionateFeatureLimit : -1;
}
private boolean shouldDrawWMSOverlay(Layer layer, Envelope box) {
// should draw based on the mode:
// full: no
// hybrid: yes
// overview: is the non-regionated feature count for this tile above the cutoff?
if (!isVectorLayer(layer))
return true;
String overlayMode = context.getSuperOverlayMode();
if ("hybrid".equals(overlayMode) || "raster".equals(overlayMode))
return true;
if ("overview".equals(overlayMode))
return featuresInTile(layer, box, false) > getRegionateFeatureLimit(getFeatureTypeInfo(layer));
return false;
}
@SuppressWarnings("rawtypes")
void encodeKMLLink(Folder container, Layer layer, String name, int drawOrder, Envelope box) {
// copy the format options
CaseInsensitiveMap fo = new CaseInsensitiveMap(new HashMap());
fo.putAll(mapContent.getRequest().getFormatOptions());
// we want to pass through format options except for superoverlay, we need to
// turn it off so we get actual placemarks back, and not more links
fo.remove("superoverlay");
// get the regionate mode
String overlayMode = (String) fo.get("overlayMode");
if ("overview".equalsIgnoreCase(overlayMode)) {
// overview mode, turn off regionation
fo.remove("regionateBy");
} else {
// specify regionateBy=auto if not specified
if (!fo.containsKey("regionateBy")) {
fo.put("regionateBy", "auto");
}
}
String foEncoded = WMSRequests.encodeFormatOptions(fo);
// encode the link
NetworkLink nl = container.createAndAddNetworkLink();
nl.setName(name);
addRegion(nl, box, 128, -1);
nl.setVisibility(true);
Link link = nl.createAndSetLink();
String url = WMSRequests.getGetMapUrl(request, layer, 0, box, new String[] { "width",
"256", "height", "256", "format_options", foEncoded, "superoverlay", "true"});
link.setHref(url);
}
boolean isVectorLayer(Layer layer) {
int index = mapContent.layers().indexOf(layer);
MapLayerInfo info = mapContent.getRequest().getLayers().get(index);
return (info.getType() == MapLayerInfo.TYPE_VECTOR || info.getType() == MapLayerInfo.TYPE_REMOTE_VECTOR);
}
private FeatureTypeInfo getFeatureTypeInfo(Layer layer) {
for (MapLayerInfo info : mapContent.getRequest().getLayers())
if (info.getName().equals(layer.getTitle()))
return info.getFeature();
return null;
}
@SuppressWarnings("unchecked")
private int featuresInTile(Layer layer, Envelope bounds, boolean regionate) {
if (!isVectorLayer(layer))
return 1; // for coverages, we want raster tiles everywhere
Envelope originalBounds = mapContent.getRequest().getBbox();
mapContent.getRequest().setBbox(bounds);
mapContent.getViewport().setBounds(
new ReferencedEnvelope(bounds, DefaultGeographicCRS.WGS84));
String originalRegionateBy = null;
if (regionate) {
originalRegionateBy = (String) mapContent.getRequest().getFormatOptions()
.get("regionateby");
if (originalRegionateBy == null)
mapContent.getRequest().getFormatOptions().put("regionateby", "auto");
}
int numFeatures = 0;
try {
numFeatures = new KMLFeatureAccessor().getFeatureCount(layer, mapContent, wms, -1);
} catch (ServiceException e) {
LOGGER.severe("Caught the WmsException!");
numFeatures = -1;
} catch (HttpErrorCodeException e) {
if (e.getErrorCode() == 204) {
throw e;
} else {
LOGGER.log(Level.WARNING, "Failure while checking whether a regionated child tile "
+ "contained features!", e);
}
} catch (Exception e) {
// Probably just trying to regionate a raster layer...
LOGGER.log(Level.WARNING,
"Failure while checking whether a regionated child tile contained features!", e);
}
mapContent.getRequest().setBbox(originalBounds);
mapContent.getViewport().setBounds(
new ReferencedEnvelope(originalBounds, DefaultGeographicCRS.WGS84));
if (regionate && originalRegionateBy == null) {
mapContent.getRequest().getFormatOptions().remove("regionateby");
}
return numFeatures;
}
void encodeGroundOverlay(Folder container, Layer layer, int drawOrder, Envelope box) {
GroundOverlay go = container.createAndAddGroundOverlay();
go.setDrawOrder(drawOrder);
Icon icon = go.createAndSetIcon();
String href = WMSRequests.getGetMapUrl(request, layer, 0, box, new String[] { "width",
"256", "height", "256", "format", "image/png", "transparent", "true" });
icon.setHref(href);
LOGGER.fine(href);
// make sure the ground overlay disappears as the lower tiles activate
addRegion(go, box, 128, 512);
LatLonBox llBox = go.createAndSetLatLonBox();
setEnvelope(box, llBox);
}
/**
* Returns true if the request is GWC compatible
*
* @param mapContent
*
*/
@SuppressWarnings("unchecked")
private boolean isRequestGWCCompatible(GetMapRequest request, int layerIndex, WMS wms) {
// check the kml params are the same as the defaults (GWC uses always the defaults)
boolean requestKmAttr = context.isDescriptionEnabled();
if (requestKmAttr != wms.getKmlKmAttr()) {
return false;
}
boolean requestKmplacemark = context.isPlacemarkForced();
if (requestKmplacemark != wms.getKmlPlacemark()) {
return false;
}
int requestKmscore = context.getKmScore();
if (requestKmscore != wms.getKmScore()) {
return false;
}
// check the layer is local
if (request.getLayers().get(layerIndex).getType() == MapLayerInfo.TYPE_REMOTE_VECTOR) {
return false;
}
// check the layer is using the default style
Style requestedStyle = request.getStyles().get(layerIndex);
Style defaultStyle = request.getLayers().get(layerIndex).getDefaultStyle();
if (!defaultStyle.equals(requestedStyle)) {
return false;
}
// check there is no extra filtering applied to the layer
List<Filter> filters = request.getFilter();
if (filters != null && filters.size() > 0 && filters.get(layerIndex) != Filter.INCLUDE) {
return false;
}
// no fiddling with antialiasing settings
String antialias = (String) request.getFormatOptions().get("antialias");
if (antialias != null && !"FULL".equalsIgnoreCase(antialias)) {
return false;
}
// no custom palette
if (request.getPalette() != null) {
return false;
}
// no custom start index
if (request.getStartIndex() != null && request.getStartIndex() != 0) {
return false;
}
// no custom max features
if (request.getMaxFeatures() != null) {
return false;
}
// no sql view params
if (request.getViewParams() != null && request.getViewParams().size() > 0) {
return false;
}
// ok, it seems everything is the same as GWC cached it
return true;
}
}