/* Copyright (c) 2001 - 2008 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 static org.geoserver.ows.util.ResponseUtils.appendPath;
import static org.geoserver.ows.util.ResponseUtils.buildURL;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.config.GeoServer;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.wms.GetMapRequest;
import org.geoserver.wms.WMS;
import org.geoserver.wms.WMSMapContext;
import org.geoserver.wms.featureinfo.FeatureHeightTemplate;
import org.geoserver.wms.featureinfo.FeatureTemplate;
import org.geoserver.wms.featureinfo.FeatureTimeTemplate;
import org.geotools.data.DataUtilities;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.type.DateUtil;
import org.geotools.map.MapLayer;
import org.geotools.renderer.style.ExpressionExtractor;
import org.geotools.renderer.style.LineStyle2D;
import org.geotools.renderer.style.MarkStyle2D;
import org.geotools.renderer.style.PolygonStyle2D;
import org.geotools.renderer.style.SLDStyleFactory;
import org.geotools.renderer.style.Style2D;
import org.geotools.renderer.style.TextStyle2D;
import org.geotools.styling.ExternalGraphic;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.LineSymbolizer;
import org.geotools.styling.Mark;
import org.geotools.styling.PointSymbolizer;
import org.geotools.styling.PolygonSymbolizer;
import org.geotools.styling.Rule;
import org.geotools.styling.SLD;
import org.geotools.styling.Symbolizer;
import org.geotools.styling.TextSymbolizer;
import org.geotools.util.NumberRange;
import org.geotools.xs.bindings.XSDateTimeBinding;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Expression;
import org.xml.sax.ContentHandler;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateFilter;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineSegment;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* Common utility adapter for kml raster/vector transformers.
*
* @author Wayne Fang, Refractions Research, wfang@refractions.net
* @author Arne Kepp - OpenGeo
* @author Justin Deoliveira - OpenGeo
*
* @version $Id: KMLMapTransformer.java 14886 2010-09-22 02:26:43Z groldan $
*/
public abstract class KMLMapTransformer extends KMLTransformerBase {
/**
* logger
*/
static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger("org.geoserver.kml");
private static final FilterFactory ff = CommonFactoryFinder.getFilterFactory(null);
/**
* The scale denominator.
*
* TODO: calcuate a real value based on image size to bbox ratio, as image
* size has no meanining for KML yet this is a fudge.
*/
protected double scaleDenominator = 1;
NumberRange scaleRange = new NumberRange(scaleDenominator, scaleDenominator);
/**
* used to create 2d style objects for features
*/
SLDStyleFactory styleFactory = new SLDStyleFactory();
/**
* Feature template, cached for performance reasons
*/
FeatureTemplate template = new FeatureTemplate();
/**
* The map context
*/
protected WMSMapContext mapContext;
/**
* The map layer being transformed
*/
protected final MapLayer mapLayer;
/**
* Whether vector name and description should be generated or not
*/
protected final boolean vectorNameDescription;
protected WMS wms;
/**
* list of formats which correspond to the default formats in which
* freemarker outputs dates when a user calls the
* ?datetime(),?date(),?time() fuctions.
*/
static List/* <SimpleDateFormat> */dtformats = new ArrayList();
static List/* <SimpleDateFormat> */dformats = new ArrayList();
static List/* <SimpleDateFormat> */tformats = new ArrayList();
static {
// add default freemarker ones first since they are likely to be used
// first, the order of this list matters.
dtformats.add(DateFormat.getDateTimeInstance());
dtformats.add(FeatureTemplate.DATETIME_FORMAT);
addFormats(dtformats, "dd%MM%yy hh:mm:ss");
addFormats(dtformats, "MM%dd%yy hh:mm:ss");
// addFormats(formats,"yy%MM%dd hh:mm:ss" );
addFormats(dtformats, "dd%MMM%yy hh:mm:ss");
addFormats(dtformats, "MMM%dd%yy hh:mm:ss");
// addFormats(formats,"yy%MMM%dd hh:mm:ss" );
addFormats(dtformats, "dd%MM%yy hh:mm");
addFormats(dtformats, "MM%dd%yy hh:mm");
// addFormats(formats,"yy%MM%dd hh:mm" );
addFormats(dtformats, "dd%MMM%yy hh:mm");
addFormats(dtformats, "MMM%dd%yy hh:mm");
// addFormats(formats,"yy%MMM%dd hh:mm" );
dformats.add(DateFormat.getDateInstance());
dformats.add(FeatureTemplate.DATE_FORMAT);
addFormats(dformats, "dd%MM%yy");
addFormats(dformats, "MM%dd%yy");
// addFormats(formats,"yy%MM%dd" );
addFormats(dformats, "dd%MMM%yy");
addFormats(dformats, "MMM%dd%yy");
// addFormats(formats,"yy%MMM%dd" );
tformats.add(DateFormat.getTimeInstance());
tformats.add(FeatureTemplate.TIME_FORMAT);
}
static void addFormats(List formats, String pattern) {
formats.add(new SimpleDateFormat(pattern.replaceAll("%", "-")));
formats.add(new SimpleDateFormat(pattern.replaceAll("%", "/")));
formats.add(new SimpleDateFormat(pattern.replaceAll("%", ".")));
formats.add(new SimpleDateFormat(pattern.replaceAll("%", " ")));
formats.add(new SimpleDateFormat(pattern.replaceAll("%", ",")));
}
public KMLMapTransformer(WMS wms, WMSMapContext mapContext, MapLayer mapLayer) {
this.wms = wms;
this.mapContext = mapContext;
this.mapLayer = mapLayer;
this.vectorNameDescription = KMLUtils.getKMAttr(mapContext.getRequest(), wms);
}
public abstract class KMLMapTranslatorSupport extends KMLTranslatorSupport {
/**
* Geometry transformer
*/
KMLGeometryTransformer.KMLGeometryTranslator geometryTranslator;
public KMLMapTranslatorSupport(ContentHandler contentHandler) {
super(contentHandler);
}
/**
* Encodes a KML Placemark name from a feature by processing a template.
*/
protected void encodePlacemarkName(SimpleFeature feature, List<Symbolizer> symbolizers)
throws IOException {
// Algorithm for finding name / label of a placemark
// 1. The title template for feature
// 2. If the title is the same as the fid
// - try getting something better from the SLD
// 3. Add <name> with whatever we've got, fid is worst case
String title = template.title(feature);
boolean trySLD = false;
if (title == null || "".equals(title)) {
title = feature.getID();
}
if(title.equals(feature.getID())) {
trySLD = true;
}
if(trySLD) {
StringBuffer label = new StringBuffer();
for (Symbolizer sym : symbolizers) {
if (sym instanceof TextSymbolizer) {
Expression e = SLD.textLabel((TextSymbolizer) sym);
Object object = null;
if (e != null)
object = e.evaluate(feature);
String value = null;
if (object instanceof String) {
value = (String) object;
} else {
if (object != null) {
value = object.toString();
}
}
if ((value != null) && !"".equals(value.trim())) {
label.append(value);
}
}
}
if (label.length() > 0) {
title = label.toString();
}
}
start("name");
cdata(title);
end("name");
}
/**
* Encodes the Snipped element
*
* @param feature
* @param styles
*/
protected void encodePlacemarkSnippet(SimpleFeature feature, List<Symbolizer> styles) {
// does nothing at the moment
}
/**
* Encodes a KML Placemark description from a feature
*/
protected void encodePlacemarkDescription(SimpleFeature feature, List<Symbolizer> styles)
throws IOException {
StringBuilder description = new StringBuilder(template.description(feature));
try{
// just see if the geosearch module is loaded. HACK! blame dwinslow@opengeo.org
Class.forName("org.geoserver.geosearch.LayerAboutPage");
description.append("<div> <a href=\"")
.append(getFeatureTypeURL())
.append(".html")
.append("\">Full dataset info and download</a> </div>");
} catch (ClassNotFoundException cnfe) {
/* don't do anything, the link is already omitted */
}
if (description != null) {
start("description");
cdata(description.toString());
end("description");
}
}
/**
* Encodes a KML Placemark LookAt from a geometry + centroid.
*/
protected void encodePlacemarkLookAt(Coordinate centroid) {
start("LookAt");
element("longitude", Double.toString(centroid.x));
element("latitude", Double.toString(centroid.y));
element("heading", "10.0");
element("tilt", "10.0");
element("range", "700");
end("LookAt");
}
/**
* Extract the symbolizers for a particular feature from a list of styles.
*
* @param feature the SimpleFeature for which symbolizers should be extracted
* @param styles an array of FeatureTypeStyle's to be filtered
* @return a List<Symbolizer> containing only the symbolizers that apply to this placemark
*/
protected List<Symbolizer> filterSymbolizers(
SimpleFeature feature,
FeatureTypeStyle[] styles
) {
// encode the Line/Poly styles
List<Symbolizer> symbolizerList = new ArrayList<Symbolizer>();
for (int j = 0; j < styles.length; j++) {
Rule[] rules = KMLUtils.filterRules(styles[j], feature, scaleDenominator);
for (int i = 0; i < rules.length; i++) {
symbolizerList.addAll(Arrays.asList(rules[i].getSymbolizers()));
}
}
return symbolizerList;
}
/**
* Encode a KML Style for a particular feature.
*
* @param feature the SimpleFeature whose style is being encoded. This is needed to help
* with guessing default values if none are specified by the style.
* @param symbolizers a list of Symbolizers which apply to the feature.
*/
protected void encodeStyle(SimpleFeature feature, List<Symbolizer> symbolizers) {
if (!symbolizers.isEmpty()) {
// start the style
start("Style");
Symbolizer[] symbolizerArray =
(Symbolizer[]) symbolizers.toArray(new Symbolizer[symbolizers.size()]);
encodeStyle(feature, symbolizerArray);
// end the style
end("Style");
}
}
/**
* Encodes an IconStyle for a feature.
*/
protected void encodeDefaultIconStyle(SimpleFeature feature) {
// figure out if line or polygon
boolean line = feature.getDefaultGeometry() != null
&& (feature.getDefaultGeometry() instanceof LineString || feature
.getDefaultGeometry() instanceof MultiLineString);
boolean poly = feature.getDefaultGeometry() != null
&& (feature.getDefaultGeometry() instanceof Polygon || feature
.getDefaultGeometry() instanceof MultiPolygon);
// Final pre-flight check
if (!line && !poly) {
LOGGER.log(
Level.FINER,
"Unexpectedly entered encodeDefaultIconStyle() "
+ "with something that does not have a multipoint geometry.");
return;
}
// start IconStyle
start("IconStyle");
// make transparent if they ask for attributes, since we'll have a label
if (vectorNameDescription) {
encodeColor("00ffffff");
}
// if line or polygon scale the label
if (line || poly) {
element("scale", "0.4");
}
// start Icon
start("Icon");
// Note the version number in case we want to replace the icon
String imageURL = "http://icons.opengeo.org/markers/icon-"
+ (poly ? "poly.1" : "line.1") + ".png";
element("href", imageURL);
end("Icon");
// end IconStyle
end("IconStyle");
}
/**
* Encodes a transparent KML LabelStyle
*/
protected void encodeDefaultTextStyle() {
start("LabelStyle");
encodeColor("00ffffff");
end("LabelStyle");
}
/**
* Encodes the provided set of symbolizers as KML styles.
*/
protected void encodeStyle(SimpleFeature feature, Symbolizer[] symbolizers) {
try {
/**
* This causes some performance overhead, but we should separate
* out repeated styles anyway...
*
* In order of appearance, according to KML specs
*/
LinkedList<PointSymbolizer> iconStyles = new LinkedList<PointSymbolizer>();
LinkedList<TextSymbolizer> labelStyles = new LinkedList<TextSymbolizer>();
LinkedList<LineSymbolizer> lineStyles = new LinkedList<LineSymbolizer>();
LinkedList<PolygonSymbolizer> polyStyles = new LinkedList<PolygonSymbolizer>();
// * Not used: <kml:BalloonStyle>
// * Not used: <kml:ListStyle>
for (int i = 0; i < symbolizers.length; i++) {
Symbolizer sym = symbolizers[i];
if (sym instanceof PointSymbolizer) {
iconStyles.add((PointSymbolizer) sym);
} else if (sym instanceof TextSymbolizer) {
labelStyles.add((TextSymbolizer) sym);
} else if (sym instanceof LineSymbolizer) {
lineStyles.add((LineSymbolizer) sym);
} else if (sym instanceof PolygonSymbolizer) {
polyStyles.add((PolygonSymbolizer) sym);
}
LOGGER.finer(new StringBuffer("Adding symbolizer ").append(
sym).toString());
}
// Points / Icons
if (iconStyles.isEmpty()) {
// Add a default point symbolizer, so people have something
// to click on
encodeDefaultIconStyle(feature);
} else {
Iterator<PointSymbolizer> iter = iconStyles.iterator();
while (iter.hasNext()) {
PointSymbolizer sym = (PointSymbolizer) iter.next();
try {
Style2D style = styleFactory.createStyle(feature,
sym, scaleRange);
encodePointStyle(feature, style, sym);
} catch (IllegalArgumentException iae) {
LOGGER.fine(iae.getMessage() + " for "
+ sym.toString());
}
}
}
// Labels / Text
if (labelStyles.isEmpty()) {
encodeDefaultTextStyle();
} else {
Iterator<TextSymbolizer> iter = labelStyles.iterator();
while (iter.hasNext()) {
TextSymbolizer sym = (TextSymbolizer) iter.next();
try {
TextStyle2D style = (TextStyle2D) styleFactory
.createStyle(feature, sym, scaleRange);
encodeTextStyle(feature, style, sym);
} catch (IllegalArgumentException iae) {
LOGGER.fine(iae.getMessage() + " for "
+ sym.toString());
}
}
}
// Lines
if (!lineStyles.isEmpty()) {
Iterator<LineSymbolizer> iter = lineStyles.iterator();
while (iter.hasNext()) {
LineSymbolizer sym = (LineSymbolizer) iter.next();
try {
LineStyle2D style = (LineStyle2D) styleFactory
.createStyle(feature, sym, scaleRange);
encodeLineStyle(feature, style, sym);
} catch (IllegalArgumentException iae) {
LOGGER.fine(iae.getMessage() + " for "
+ sym.toString());
}
}
}
// Polygons
if (!polyStyles.isEmpty()) {
Iterator<PolygonSymbolizer> iter = polyStyles.iterator();
while (iter.hasNext()) {
PolygonSymbolizer sym = (PolygonSymbolizer) iter.next();
try {
PolygonStyle2D style = (PolygonStyle2D) styleFactory
.createStyle(feature, sym, scaleRange);
// The last argument is forced outline
encodePolygonStyle(feature, style, sym, !lineStyles
.isEmpty());
} catch (IllegalArgumentException iae) {
LOGGER.fine(iae.getMessage() + " for "
+ sym.toString());
}
}
}
} catch (Exception e) {
LOGGER.log(Level.WARNING,
"Error occurred during style encoding", e);
}
}
/**
* Encodes a KML IconStyle + PolyStyle from a polygon style and
* symbolizer.
*/
protected void encodePolygonStyle(SimpleFeature feature, PolygonStyle2D style,
PolygonSymbolizer symbolizer, boolean forceOutline) {
// star the polygon style
start("PolyStyle");
// fill
if (symbolizer.getFill() != null) {
// get opacity
double opacity = ((Number)symbolizer.getFill().getOpacity().evaluate(feature)).doubleValue();
if (Double.isNaN(opacity)) {
// none specified, default to full opacity
opacity = 1.0;
}
encodeColor(
(Color)symbolizer.getFill().getColor().evaluate(feature, Color.class),
opacity
);
} else {
// make it transparent
encodeColor("00aaaaaa");
}
// outline
if (symbolizer.getStroke() != null || forceOutline) {
element("outline", "1");
} else {
element("outline", "0");
}
end("PolyStyle");
// if stroke specified add line style as well
if (symbolizer.getStroke() != null) {
start("LineStyle");
// opacity
double opacity =
((Number)symbolizer.getStroke().getOpacity().evaluate(feature)).doubleValue();
if (Double.isNaN(opacity)) {
// none specified, default to full opacity
opacity = 1.0;
}
if (style != null) {
encodeColor(
KMLUtils.colorToHex(
(Color)
symbolizer.getStroke().getColor().evaluate(feature, Color.class),
opacity
)
);
}
// width
int width = ((Number)symbolizer.getStroke().getWidth().evaluate(feature)).intValue();
if (width != SLD.NOTFOUND) {
element("width", Integer.toString(width));
}
end("LineStyle");
}
}
/**
* Encodes a KML IconStyle + LineStyle from a polygon style and
* symbolizer.
*/
protected void encodeLineStyle(SimpleFeature feature, LineStyle2D style,
LineSymbolizer symbolizer) {
start("LineStyle");
// stroke
if (symbolizer.getStroke() != null) {
// opacity
double opacity = ((Number)symbolizer.getStroke().getOpacity().evaluate(feature)).doubleValue();
if (Double.isNaN(opacity)) {
// default to full opacity
opacity = 1.0;
}
if (symbolizer.getStroke().getColor() != null) {
encodeColor((Color)symbolizer.getStroke().getColor().evaluate(feature, Color.class), opacity);
} else if (style != null) {
encodeColor((Color) style.getContour(), opacity);
}
// width
int width = SLD.width(symbolizer.getStroke());
if (width != SLD.NOTFOUND) {
element("width", Integer.toString(width));
}
} else {
// default
encodeColor("ffaaaaaa");
element("width", "1");
}
end("LineStyle");
}
/**
* Encodes a KML IconStyle from a point style and symbolizer.
*/
protected void encodePointStyle(SimpleFeature feature, Style2D style,
PointSymbolizer symbolizer) {
start("IconStyle");
if (style instanceof MarkStyle2D) {
Mark mark = SLD.mark(symbolizer);
if (mark != null) {
double opacity = ((Number)mark.getFill().getOpacity().evaluate(feature)).doubleValue();
if (Double.isNaN(opacity)) {
// default to full opacity
opacity = 1.0;
}
if (mark.getFill() != null) {
final Color color = (Color)mark.getFill().getColor().evaluate(feature, Color.class);
encodeColor(color, opacity);
}
}
}
element("colorMode", "normal");
// placemark icon
String iconHref = null;
// if the point symbolizer uses an external graphic use it
if ((symbolizer.getGraphic() != null)
&& (symbolizer.getGraphic().getExternalGraphics() != null)
&& (symbolizer.getGraphic().getExternalGraphics().length > 0)) {
ExternalGraphic graphic = symbolizer.getGraphic()
.getExternalGraphics()[0];
try {
if ("file".equals(graphic.getLocation().getProtocol())) {
// it is a local file, reference locally from "styles"
// directory
File file = DataUtilities.urlToFile(graphic.getLocation());
File styles = null;
File graphicFile = null;
if(file.isAbsolute()) {
GeoServerDataDirectory dataDir = (GeoServerDataDirectory) GeoServerExtensions.bean("dataDirectory");
// we grab the canonical path to make sure we can compare them, no relative parts in them and so on
styles = dataDir.findOrCreateStyleDir().getCanonicalFile();
graphicFile = file.getCanonicalFile();
file = graphicFile;
if(file.getAbsolutePath().startsWith(styles.getAbsolutePath())) {
// ok, part of the styles directory, extract only the relative path
file = new File(file.getAbsolutePath().substring(styles.getAbsolutePath().length() + 1));
} else {
// we wont' transform this, other dirs are not published
file = null;
}
}
if (file != null && styles != null) {
iconHref = ResponseUtils.buildURL(mapContext.getRequest().getBaseUrl(),
"styles/" + styles.toURI().relativize(graphicFile.toURI()),
null, URLType.RESOURCE);
}
} else if ("http".equals(graphic.getLocation()
.getProtocol())) {
iconHref = graphic.getLocation().toString();
} else {
// TODO: should we check for http:// and use it
// directly?
// other protocols?
}
} catch (Exception e) {
LOGGER.log(Level.WARNING,
"Error processing external graphic:" + graphic, e);
}
}
iconHref = evaluateDynamicSymbolizer(iconHref, feature);
if (iconHref == null) {
iconHref = "http://maps.google.com/mapfiles/kml/pal4/icon25.png";
}
start("Icon");
element("href", iconHref);
end("Icon");
end("IconStyle");
}
/**
* Does value substitution on a URL with embedded CQL expressions
* @param strLocation the URL as a string, possibly with expressions
* @param feature the feature providing the context in which the expressions are evaluated
* @return a string containing the final URL
*/
protected String evaluateDynamicSymbolizer(String strLocation, SimpleFeature feature){
if (strLocation == null) return null;
// parse the eventual ${cqlExpression} embedded in the URL
Expression location;
try {
location = ExpressionExtractor.extractCqlExpressions(strLocation);
} catch(IllegalArgumentException e) {
// in the unlikely event that a URL is using one of the chars reserved for ${cqlExpression}
// let's try and use the location as a literal
if(LOGGER.isLoggable(Level.SEVERE))
LOGGER.log(Level.SEVERE, "Could not parse cql expressions out of " + strLocation, e);
location = ff.literal(strLocation);
}
return location.evaluate(feature).toString();
}
/**
* Encodes a KML LabelStyle from a text style and symbolizer.
*/
protected void encodeTextStyle(SimpleFeature feature, TextStyle2D style,
TextSymbolizer symbolizer) {
start("LabelStyle");
if (symbolizer.getFill() != null) {
double opacity = ((Number)symbolizer.getFill().getOpacity().evaluate(feature)).doubleValue();
if (Double.isNaN(opacity)) {
// default to full opacity
opacity = 1.0;
}
encodeColor((Color)symbolizer.getFill().getColor().evaluate(feature, Color.class), opacity);
} else {
// default
encodeColor("ffffffff");
}
end("LabelStyle");
}
/**
* Encodes a color element from its color + opacity representation.
*
* @param color
* The color to encode.
* @param opacity
* The opacity ( alpha ) of the color.
*/
void encodeColor(Color color, double opacity) {
encodeColor(KMLUtils.colorToHex(color, opacity));
}
/**
* Encodes a color element from its hex representation.
*
* @param hex
* The hex value ( with alpha ) of the color.
*
*/
void encodeColor(String hex) {
element("color", hex);
}
/**
* Returns the centroid of the geometry, handling a geometry collection.
* <p>
* In the case of a collection a multi point containing the centroid of
* each geometry in the collection is calculated. The first point in the
* multi point is returned as the cetnroid.
* </p>
*/
Coordinate geometryCentroid(Geometry g) {
// TODO: should the collection case return the centroid of the
// multi point?
if (g instanceof GeometryCollection) {
GeometryCollection gc = (GeometryCollection) g;
// check for case of single geometry
if (gc.getNumGeometries() == 1) {
g = gc.getGeometryN(0);
} else {
double maxAreaSoFar = gc.getGeometryN(0).getArea();
Coordinate centroidToReturn = gc.getGeometryN(0).getCentroid().getCoordinate();
for (int t = 0; t < gc.getNumGeometries(); t++) {
double area = gc.getGeometryN(t).getArea();
if (area > maxAreaSoFar) {
maxAreaSoFar = area;
centroidToReturn = gc.getGeometryN(t).getCentroid().getCoordinate();
}
}
return centroidToReturn;
}
}
if (g instanceof Point) {
// thats easy
return g.getCoordinate();
} else if (g instanceof LineString) {
// make sure the point we return is actually on the line
double tol = 1E-6;
double mid = g.getLength() / 2d;
Coordinate[] coords = g.getCoordinates();
// walk along the linestring until we get to a point where we
// have two coordinates that straddle the midpoint
double len = 0d;
for (int i = 1; i < coords.length; i++) {
LineSegment line = new LineSegment(coords[i - 1], coords[i]);
len += line.getLength();
if (Math.abs(len - mid) < tol) {
// close enough
return line.getCoordinate(1);
}
if (len > mid) {
// we have gone past midpoint
return line.pointAlong(1 - ((len - mid) / line
.getLength()));
}
}
// should never get there
return g.getCentroid().getCoordinate();
} else {
// return the actual centroid
return g.getCentroid().getCoordinate();
}
}
protected void encodePlacemark(SimpleFeature feature, List<Symbolizer> symbolizers) {
encodePlacemark(feature, symbolizers, null);
}
/**
* Encodes a KML Placemark from a feature and optional name.
*/
protected void encodePlacemark(
SimpleFeature feature,
List<Symbolizer> symbolizers,
Geometry markGeometry
) {
Geometry geometry = featureGeometry(feature);
Coordinate centroid = geometryCentroid(geometry);
start("Placemark", KMLUtils.attributes(new String[] { "id", feature.getID() }));
// encode name + description only if kmattr was specified
if (vectorNameDescription) {
// name
try {
encodePlacemarkName(feature, symbolizers);
} catch (Exception e) {
String msg = "Error occured processing 'title' template.";
LOGGER.log(Level.WARNING, msg, e);
}
// snippet (only used by OWS5 prototype at the moment)
try {
encodePlacemarkSnippet(feature, symbolizers);
} catch (Exception e) {
String msg = "Error occured processing 'description' template.";
LOGGER.log(Level.WARNING, msg, e);
}
// description
try {
encodePlacemarkDescription(feature, symbolizers);
} catch (Exception e) {
String msg = "Error occured processing 'description' template.";
LOGGER.log(Level.WARNING, msg, e);
}
}
String selfLinks = (String) mapContext.getRequest()
.getFormatOptions().get("selfLinks");
if (selfLinks != null && selfLinks.equalsIgnoreCase("true")) {
GetMapRequest request = mapContext.getRequest();
String link = "";
try {
link = getFeatureTypeURL();
} catch (IOException ioe) {
/* what could *possibly* go wrong? */
throw new RuntimeException(ioe);
}
String[] id = feature.getID().split("\\.");
link = link + "/" + id[1] + ".kml";
element("atom:link", null, KMLUtils.attributes(new String[] {
"rel", "self", "href", link }));
}
// look at
encodePlacemarkLookAt(centroid);
// time
try {
encodePlacemarkTime(feature, symbolizers);
} catch (Exception e) {
String msg = "Error occured processing 'time' template: "
+ e.getMessage();
LOGGER.log(Level.WARNING, msg);
LOGGER.log(Level.FINE, "", e);
}
encodeStyle(feature, symbolizers);
// encode extended data (kml 2.2)
encodeExtendedData(feature);
// geometry
if (markGeometry == null) {
Coordinate labelPoint = vectorNameDescription ? centroid : null;
encodePlacemarkGeometry(geometry, labelPoint, symbolizers);
} else {
// if given a specific placemark geometry, encode a point
// at the geometry coordinates
Coordinate markCentroid = markGeometry.getCoordinate();
start("Point");
if (!Double.isNaN(markCentroid.z)) {
element("coordinates", markCentroid.x + ","
+ markCentroid.y + "," + markCentroid.z);
} else {
element("coordinates", markCentroid.x + ","
+ markCentroid.y);
}
end("Point");
}
end("Placemark");
}
/**
* Encodes kml 2.2 extended data section
*
* @param feature
*/
protected void encodeExtendedData(SimpleFeature feature) {
// code at the moment is in KML3VectorTransfomer
}
protected String getFeatureTypeURL() throws IOException {
GeoServer gs = wms.getGeoServer();
Catalog catalog = gs.getCatalog();
String nsUri = mapLayer.getFeatureSource().getSchema().getName().getNamespaceURI();
NamespaceInfo ns = catalog.getNamespaceByURI(nsUri);
String featureTypeName = mapLayer.getFeatureSource().getSchema()
.getName().getLocalPart();
GetMapRequest request = mapContext.getRequest();
String baseURL = request.getBaseUrl();
String prefix = ns.getPrefix();
return buildURL(baseURL, appendPath("rest", prefix, featureTypeName), null,
URLType.SERVICE);
}
/**
* Encodes a KML TimePrimitive geometry from a feature.
*/
protected void encodePlacemarkTime(SimpleFeature feature, List<Symbolizer> symbolizers)
throws IOException {
try {
String[] time = new FeatureTimeTemplate(template)
.execute(feature);
if (time.length == 0) {
return;
}
if (time.length == 1) {
encodeKmlTimeStamp(parseDateTime(time[0]));
} else {
encodeKmlTimeSpan(parseDateTime(time[0]),
parseDateTime(time[1]));
}
} catch (Exception e) {
throw (IOException) new IOException().initCause(e);
}
}
/**
* Encodes the time pairs into a kml TimeSpan (from and to will be
* parsed into the official kml date/time representation)
*
* @param from
* @param to
* @throws Exception
*/
protected void encodeKmlTimeSpan(Date from, Date to) throws Exception {
// timespan case
String begin = encodeDateTime(from);
String end = encodeDateTime(to);
if (!(begin == null && end == null)) {
start("TimeSpan");
if (begin != null) {
element("begin", begin);
}
if (end != null) {
element("end", end);
}
end("TimeSpan");
}
}
/**
* Encodes a kml Timestamp element with provided time (which will be
* parsed into the standard kml representation)
*
* @param time
* @throws Exception
*/
protected void encodeKmlTimeStamp(Date time) throws Exception {
String datetime = encodeDateTime(time);
if (datetime != null) {
// timestamp case
start("TimeStamp");
element("when", datetime);
end("TimeStamp");
}
}
protected String encodeDateTime(Date date) {
if (date != null) {
Calendar c = Calendar.getInstance();
c.setTime(date);
return new XSDateTimeBinding().encode(c, null);
} else {
return null;
}
}
/**
* Encodes a date as an xs:dateTime.
*/
protected Date parseDateTime(String date) throws Exception {
// first try as date time
Date d = parseDate(dtformats, date);
if (d == null) {
// then try as date
d = parseDate(dformats, date);
}
if (d == null) {
// try as time
d = parseDate(tformats, date);
}
if (d == null) {
// last ditch effort, try to parse as xml dates
try {
// try as xml date time
d = DateUtil.deserializeDateTime(date);
} catch (Exception e1) {
try {
// try as xml date
d = DateUtil.deserializeDate(date);
} catch (Exception e2) {
}
}
}
if (d != null) {
return d;
}
LOGGER.warning("Could not parse date: " + date);
return null;
}
/**
* Parses a date as a string into a well-known format.
*/
protected Date parseDate(List formats, String date) {
for (Iterator f = formats.iterator(); f.hasNext();) {
SimpleDateFormat format = (SimpleDateFormat) f.next();
Date d = null;
try {
d = format.parse(date);
} catch (ParseException e) {
}
if (d != null) {
return d;
}
}
return null;
}
/**
* Encodes a KML Placemark geometry from a geometry + centroid.
*
* @param styles
*/
protected void encodePlacemarkGeometry(
Geometry geometry,
Coordinate centroid,
List<Symbolizer> symbolizers
) {
// if point, just encode a single point, otherwise encode the
// geometry + centroid
if (geometry instanceof Point || (geometry instanceof MultiPoint)
&& ((MultiPoint) geometry).getNumPoints() == 1) {
encodeGeometry( geometry, symbolizers );
} else {
start("MultiGeometry");
if (!Double.isNaN(geometry.getCoordinate().z)) {
centroid.z = geometry.getCoordinate().z;
}
if(centroid != null) {
encodeGeometry(new GeometryFactory().createPoint(centroid), null);
}
// the actual geometry
encodeGeometry(geometry, symbolizers);
end("MultiGeometry");
}
}
/**
* Encodes a KML geometry.
*
* @param styles
*/
protected void encodeGeometry(Geometry geometry, List<Symbolizer> symbolizers) {
if (geometry instanceof GeometryCollection) {
// unwrap the collection
GeometryCollection collection = (GeometryCollection) geometry;
for (int i = 0; i < collection.getNumGeometries(); i++) {
encodeGeometry(collection.getGeometryN(i), symbolizers);
}
} else if(geometry instanceof Point) {
Coordinate centroid = ((Point) geometry).getCoordinate();
start("Point");
if (!Double.isNaN(centroid.z)) {
geometryTranslator.insertExtrudeTags(geometry);
element("coordinates", centroid.x + "," + centroid.y + ","
+ centroid.z);
} else {
element("coordinates", centroid.x + "," + centroid.y);
}
end("Point");
} else {
geometryTranslator.encode(geometry);
}
}
/**
* Returns the id of the feature removing special characters like
* '&','>','<','%'.
*/
String featureId(SimpleFeature feature) {
String id = feature.getID();
id = id.replaceAll("&", "");
id = id.replaceAll(">", "");
id = id.replaceAll("<", "");
id = id.replaceAll("%", "");
return id;
}
/**
* Returns the geometry for the feature reprojecting if necessary.
*/
Geometry featureGeometry(SimpleFeature f) {
// get the geometry
Geometry geom = (Geometry) f.getDefaultGeometry();
try{
final double height = new FeatureHeightTemplate(template).execute(f);
if (!Double.isNaN(height) && height != 0){
geom.apply(
new CoordinateFilter(){
public void filter(Coordinate c){
c.setCoordinate(new Coordinate(c.x, c.y, height));
}
}
);
geom.geometryChanged();
}
} catch (IOException ioe){
LOGGER.log(Level.WARNING, "Couldn't render height template for " + f.getID(), ioe);
}
// rprojection done in KMLTransformer
// if (!CRS.equalsIgnoreMetadata(sourceCrs, mapContext.getCoordinateReferenceSystem())) {
// try {
// MathTransform transform = CRS.findMathTransform(sourceCrs,
// mapContext.getCoordinateReferenceSystem(), true);
// geom = JTS.transform(geom, transform);
// } catch (MismatchedDimensionException e) {
// LOGGER.severe(e.getLocalizedMessage());
// } catch (TransformException e) {
// LOGGER.severe(e.getLocalizedMessage());
// } catch (FactoryException e) {
// LOGGER.severe(e.getLocalizedMessage());
// }
// }
return geom;
}
}
}