/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.kml;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.config.ServiceInfo;
import org.geoserver.kml.decorator.KmlDecoratorFactory;
import org.geoserver.kml.decorator.KmlDecoratorFactory.KmlDecorator;
import org.geoserver.kml.sequence.CompositeList;
import org.geoserver.kml.utils.LookAtOptions;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.GetMapRequest;
import org.geoserver.wms.WMS;
import org.geoserver.wms.WMSMapContent;
import org.geoserver.wms.featureinfo.FeatureTemplate;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.FeatureLayer;
import org.geotools.map.Layer;
import org.geotools.map.MapViewport;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.styling.Style;
import org.geotools.styling.Symbolizer;
import org.geotools.util.Converters;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Envelope;
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;
/**
* A class used by {@link KmlDecorator} to get the current encoding context (request, map content,
* current layer, feature and so on).
*
* @author Andrea Aime - GeoSolutions
*/
public class KmlEncodingContext {
static final Logger LOGGER = Logging.getLogger(KmlEncodingContext.class);
protected boolean kmz;
protected WMSMapContent mapContent;
protected GetMapRequest request;
protected List<Symbolizer> currentSymbolizers;
protected Layer currentLayer;
protected SimpleFeatureCollection currentFeatureCollection;
protected SimpleFeature currentFeature;
protected Map<String, Object> metadata = new HashMap<String, Object>();
protected boolean descriptionEnabled;
protected FeatureTemplate template = new FeatureTemplate();
protected LookAtOptions lookAtOptions;
protected WMS wms;
protected Map<String, Layer> kmzGroundOverlays = new LinkedHashMap<String, Layer>();
protected boolean placemarkForced;
protected String superOverlayMode;
protected boolean superOverlayEnabled;
protected boolean networkLinksFormat;
protected boolean extendedDataEnabled;
protected int kmScore;
protected ServiceInfo service;
protected int layerIndex;
protected String mode;
/**
* Holds the feature iterators that have been opened, but not yet closed, to make sure
* they get disposed at the end of the encoding even in case of exceptions during the encoding
*/
protected IdentityHashMap<FeatureIterator, FeatureIterator> iterators = new IdentityHashMap<FeatureIterator, FeatureIterator>();
public final static ReferencedEnvelope WORLD_BOUNDS_WGS84 = new ReferencedEnvelope(-180, 180, -90, 90, DefaultGeographicCRS.WGS84);
protected boolean liveIcons;
protected Map<String, Style> iconStyles;
public KmlEncodingContext(WMSMapContent mapContent, WMS wms, boolean kmz) {
this.mapContent = fixViewport(mapContent);
this.request = mapContent.getRequest();
this.wms = wms;
this.mode = computeModeOption(request.getFormatOptions());
this.descriptionEnabled = computeKMAttr();
this.lookAtOptions = new LookAtOptions(request.getFormatOptions());
this.placemarkForced = computeKmplacemark();
this.superOverlayMode = computeSuperOverlayMode();
this.superOverlayEnabled = computeSuperOverlayEnabled();
this.extendedDataEnabled = computeExtendedDataEnabled();
this.kmScore = computeKmScore();
this.networkLinksFormat = KMLMapOutputFormat.NL_KML_MIME_TYPE.equals(request.getFormat()) || KMZMapOutputFormat.NL_KMZ_MIME_TYPE.equals(request.getFormat());
this.kmz = kmz;
this.service = wms.getServiceInfo();
this.liveIcons = true;
this.iconStyles = new HashMap<String,Style>();
Boolean autofit = Converters.convert(request.getFormatOptions().get("autofit"), Boolean.class);
if(autofit != null && Converters.convert(autofit, Boolean.class)) {
double width = mapContent.getMapWidth();
double height = mapContent.getMapHeight();
ReferencedEnvelope bbox = mapContent.getViewport().getBounds();
double bbox_width = bbox.getWidth();
double bbox_height = bbox.getHeight();
// be on the safe side
if(bbox_width > 0 && bbox_height > 0 & width > 0 & height > 0) {
double ratio = bbox_width / bbox_height;
if(bbox_width > bbox_height) {
height = width / ratio;
int h = (int) Math.ceil(height);
mapContent.setMapHeight(h);
mapContent.getRequest().setHeight(h);
} else {
width = height * ratio;
int w = (int) Math.ceil(width);
mapContent.setMapWidth(w);
mapContent.getRequest().setWidth(w);
}
}
}
}
private String computeModeOption(Map<String, String> rawKvp) {
String mode = KvpUtils.caseInsensitiveParam(rawKvp, "mode", null);
return mode;
}
/**
* Force the output to be in WGS84
* @param mc
*
*/
private WMSMapContent fixViewport(WMSMapContent mc) {
MapViewport viewport = mc.getViewport();
if(!CRS.equalsIgnoreMetadata(viewport.getCoordinateReferenceSystem(), DefaultGeographicCRS.WGS84)) {
viewport.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84);
GetMapRequest req = mc.getRequest();
req.setSRS("EPSG:4326");
req.setBbox(viewport.getBounds());
}
return mc;
}
/**
* Protected constructor used by WFS output format to create a fake kml encoding context
*/
protected KmlEncodingContext() {
}
/**
* Returns the the kmplacemark value (either specified in the request, or the default one)
*
* @param mapContent
*
*/
boolean computeKmplacemark() {
Object kmplacemark = request.getFormatOptions().get("kmplacemark");
if (kmplacemark != null) {
return Converters.convert(kmplacemark, Boolean.class);
} else {
return wms.getKmlPlacemark();
}
}
/**
* Returns the the kmattr value (either specified in the request, or the default one)
*
* @param mapContent
*
*/
boolean computeKMAttr() {
Object kmattr = request.getFormatOptions().get("kmattr");
if (kmattr == null) {
kmattr = request.getRawKvp().get("kmattr");
}
if (kmattr != null) {
return Converters.convert(kmattr, Boolean.class);
} else {
return wms.getKmlKmAttr();
}
}
private int computeKmScore() {
int kmScore = wms.getKmScore();
Map fo = request.getFormatOptions();
Object kmScoreObj = fo.get("kmscore");
if (kmScoreObj != null) {
kmScore = (Integer) kmScoreObj;
}
return kmScore;
}
/**
* Checks if the extended data is enabled or not
* @param request
*
*/
boolean computeExtendedDataEnabled() {
Map formatOptions = request.getFormatOptions();
Boolean extendedData = Converters.convert(formatOptions.get("extendedData"), Boolean.class);
if (extendedData == null) {
extendedData = Boolean.FALSE;
}
return extendedData;
}
/**
* Checks if the superoverlay is enabled or not
* @param request
*
*/
boolean computeSuperOverlayEnabled() {
Map formatOptions = request.getFormatOptions();
Boolean superoverlay = (Boolean) formatOptions.get("superoverlay");
if (superoverlay == null) {
superoverlay = Boolean.FALSE;
}
return superoverlay;
}
/**
* Returns the superoverlay mode (either specified in the request, or the default one)
*
*/
String computeSuperOverlayMode() {
String overlayMode = (String) request.getFormatOptions().get("superoverlay_mode");
if(overlayMode != null) {
return overlayMode;
}
overlayMode = (String) request.getFormatOptions().get("overlayMode");
if(overlayMode != null) {
return overlayMode;
} else {
return wms.getKmlSuperoverlayMode();
}
}
/**
* Returns the {@link KmlDecorator} objects for the specified Feature class
*
*
*/
public List<KmlDecorator> getDecoratorsForClass(Class<? extends Feature> clazz) {
List<KmlDecoratorFactory> factories = GeoServerExtensions
.extensions(KmlDecoratorFactory.class);
List<KmlDecorator> result = new ArrayList<KmlDecorator>();
for (KmlDecoratorFactory factory : factories) {
KmlDecorator decorator = factory.getDecorator(clazz, this);
if (decorator != null) {
result.add(decorator);
}
}
return result;
}
/**
* Adds features to the folder own list
*
* @param folder
* @param features
*/
public void addFeatures(Folder folder, List<Feature> features) {
List<Feature> originalFeatures = folder.getFeature();
if (originalFeatures == null || originalFeatures.size() == 0) {
folder.setFeature(features);
} else {
// in this case, compose the already existing features with the
// dynamically generated ones
folder.setFeature(new CompositeList<Feature>(originalFeatures, features));
}
}
/**
* Adds features to the document own list
*
* @param folder
* @param features
*/
public void addFeatures(Document document, List<Feature> features) {
List<Feature> originalFeatures = document.getFeature();
if (originalFeatures == null || originalFeatures.size() == 0) {
document.setFeature(features);
} else {
// in this case, compose the already existing features with the
// dynamically generated ones
document.setFeature(new CompositeList<Feature>(originalFeatures, features));
}
}
public WMSMapContent getMapContent() {
return mapContent;
}
public void setMapContent(WMSMapContent mapContent) {
this.mapContent = mapContent;
}
public GetMapRequest getRequest() {
return request;
}
public void setRequest(GetMapRequest request) {
this.request = request;
}
public List<Symbolizer> getCurrentSymbolizers() {
return currentSymbolizers;
}
public void setCurrentSymbolizers(List<Symbolizer> symbolizers) {
this.currentSymbolizers = symbolizers;
}
public Layer getCurrentLayer() {
return currentLayer;
}
public void setCurrentLayer(Layer currentLayer) {
this.currentLayer = currentLayer;
this.layerIndex++;
}
public SimpleFeature getCurrentFeature() {
return currentFeature;
}
public void setCurrentFeature(SimpleFeature currentFeature) {
this.currentFeature = currentFeature;
}
public Map<String, Object> getMetadata() {
return metadata;
}
public boolean isDescriptionEnabled() {
return descriptionEnabled;
}
public void setDescriptionEnabled(boolean descriptionEnabled) {
this.descriptionEnabled = descriptionEnabled;
}
public FeatureTemplate getTemplate() {
return template;
}
public LookAtOptions getLookAtOptions() {
return lookAtOptions;
}
public WMS getWms() {
return wms;
}
public SimpleFeatureCollection getCurrentFeatureCollection() {
return currentFeatureCollection;
}
public void setCurrentFeatureCollection(SimpleFeatureCollection currentFeatureCollection) {
this.currentFeatureCollection = currentFeatureCollection;
}
public boolean isKmz() {
return kmz;
}
/**
* Adds a layer to be generated as ground overlay in the kmz package
* @param imagePath The path of the ground overlay image inside the kmz archive
* @param layer
*/
public void addKmzGroundOverlay(String imagePath, Layer layer) {
if(!kmz) {
throw new IllegalStateException("Cannot add ground " +
"overlay layers, the output is not supposed to be a KMZ");
}
this.kmzGroundOverlays.put(imagePath, layer);
}
/**
* Returns the list of ground overlay layers to be included in the KMZ response
*
*/
public Map<String, Layer> getKmzGroundOverlays() {
return kmzGroundOverlays;
}
public boolean isPlacemarkForced() {
return placemarkForced;
}
public void setPlacemarkForced(boolean placemarkForced) {
this.placemarkForced = placemarkForced;
}
public boolean isSuperOverlayEnabled() {
return superOverlayEnabled;
}
public String getSuperOverlayMode() {
return superOverlayMode;
}
public boolean isNetworkLinksFormat() {
return networkLinksFormat;
}
public boolean isExtendedDataEnabled() {
return extendedDataEnabled;
}
public int getKmScore() {
return kmScore;
}
public ServiceInfo getService() {
return service;
}
/**
* Returns a list of the feature types to be encoded. Will provide a feature type only for the
* vector layers, a null will be placed where a layer of different nature is found
*
*/
public List<SimpleFeatureType> getFeatureTypes() {
List<SimpleFeatureType> results = new ArrayList<SimpleFeatureType>();
for(Layer layer : mapContent.layers()) {
if(layer instanceof FeatureLayer) {
results.add((SimpleFeatureType) layer.getFeatureSource().getSchema());
} else {
results.add(null);
}
}
return results;
}
/**
* Returns the current feature type is the current layer is made of vector features, null otherwise
*
*/
public SimpleFeatureType getCurrentFeatureType() {
if(currentLayer instanceof FeatureLayer) {
FeatureLayer fl = (FeatureLayer) currentLayer;
return (SimpleFeatureType) fl.getFeatureSource().getSchema();
}
return null;
}
/**
* Returns the current layer index in the request
*
*/
public int getCurrentLayerIndex() {
return layerIndex;
}
public boolean isLiveIcons() {
return liveIcons;
}
public void setLiveIcons(boolean liveIcons) {
this.liveIcons = liveIcons;
}
public Map<String, Style> getIconStyles() {
return iconStyles;
}
public String getMode() {
return mode;
}
public FeatureIterator openIterator(FeatureCollection fc) {
FeatureIterator fi = fc.features();
iterators.put(fi, fi);
return fi;
}
public void closeIterator(FeatureIterator fi) {
try {
fi.close();
} catch(Exception e) {
LOGGER.log(Level.FINE, "An exception occurred while closing a feature iterator "
+ "during the cleanup phases of the KML encoding", e);
} finally {
iterators.remove(fi);
}
}
public void closeIterators() {
// clean up any un-closed iterator
for (FeatureIterator fi : iterators.keySet()) {
try {
fi.close();
} catch(Exception e) {
LOGGER.log(Level.FINE, "An exception occurred while closing a feature iterator "
+ "during the cleanup phases of the KML encoding", e);
}
}
iterators.clear();
}
public Envelope getRequestBoxWGS84() {
try {
Envelope env = request.getBbox();
ReferencedEnvelope re;
if(env instanceof ReferencedEnvelope) {
re = (ReferencedEnvelope) env;
if(re.getCoordinateReferenceSystem() == null) {
re = new ReferencedEnvelope(re, DefaultGeographicCRS.WGS84);
}
} else {
if(request.getCrs() != null) {
re = new ReferencedEnvelope(env, request.getCrs());
} else if(request.getSRS() != null) {
CoordinateReferenceSystem crs = CRS.decode(request.getSRS());
re = new ReferencedEnvelope(env, crs);
} else {
re = new ReferencedEnvelope(env, DefaultGeographicCRS.WGS84);
}
}
if(!CRS.equalsIgnoreMetadata(re.getCoordinateReferenceSystem(), DefaultGeographicCRS.WGS84)) {
return re.transform(DefaultGeographicCRS.WGS84, true);
} else {
return re;
}
} catch(Exception e) {
throw new ServiceException("Requested bounding box " + request.getBbox() + " could not be tranformed to WGS84", e);
}
}
}