0001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
0002: * This code is licensed under the GPL 2.0 license, availible at the root
0003: * application directory.
0004: */
0005: package org.vfny.geoserver.wms.responses.map.kml;
0006:
0007: import com.vividsolutions.jts.geom.Coordinate;
0008: import com.vividsolutions.jts.geom.Geometry;
0009: import com.vividsolutions.jts.geom.GeometryCollection;
0010: import com.vividsolutions.jts.geom.LineSegment;
0011: import com.vividsolutions.jts.geom.LineString;
0012: import com.vividsolutions.jts.geom.MultiLineString;
0013: import com.vividsolutions.jts.geom.MultiPoint;
0014: import com.vividsolutions.jts.geom.MultiPolygon;
0015: import com.vividsolutions.jts.geom.Point;
0016: import com.vividsolutions.jts.geom.Polygon;
0017: import org.geoserver.ows.util.RequestUtils;
0018: import org.geotools.feature.Feature;
0019: import org.geotools.feature.FeatureCollection;
0020: import org.geotools.feature.FeatureIterator;
0021: import org.geotools.feature.FeatureType;
0022: import org.geotools.feature.type.DateUtil;
0023: import org.geotools.geometry.jts.JTS;
0024: import org.geotools.map.MapLayer;
0025: import org.geotools.referencing.CRS;
0026: import org.geotools.referencing.crs.DefaultGeographicCRS;
0027: import org.geotools.renderer.style.LineStyle2D;
0028: import org.geotools.renderer.style.MarkStyle2D;
0029: import org.geotools.renderer.style.PolygonStyle2D;
0030: import org.geotools.renderer.style.SLDStyleFactory;
0031: import org.geotools.renderer.style.Style2D;
0032: import org.geotools.renderer.style.TextStyle2D;
0033: import org.geotools.styling.ExternalGraphic;
0034: import org.geotools.styling.FeatureTypeStyle;
0035: import org.geotools.styling.LineSymbolizer;
0036: import org.geotools.styling.Mark;
0037: import org.geotools.styling.PointSymbolizer;
0038: import org.geotools.styling.PolygonSymbolizer;
0039: import org.geotools.styling.Rule;
0040: import org.geotools.styling.SLD;
0041: import org.geotools.styling.Style;
0042: import org.geotools.styling.Symbolizer;
0043: import org.geotools.styling.TextSymbolizer;
0044: import org.geotools.util.Converters;
0045: import org.geotools.util.NumberRange;
0046: import org.geotools.xml.SimpleBinding;
0047: import org.geotools.xml.transform.TransformerBase;
0048: import org.geotools.xml.transform.Translator;
0049: import org.geotools.xs.bindings.XSDateBinding;
0050: import org.geotools.xs.bindings.XSDateTimeBinding;
0051: import org.geotools.xs.bindings.XSTimeBinding;
0052: import org.opengis.filter.Filter;
0053: import org.opengis.filter.expression.Expression;
0054: import org.opengis.geometry.MismatchedDimensionException;
0055: import org.opengis.referencing.FactoryException;
0056: import org.opengis.referencing.crs.CoordinateReferenceSystem;
0057: import org.opengis.referencing.operation.MathTransform;
0058: import org.opengis.referencing.operation.TransformException;
0059: import org.vfny.geoserver.global.GeoServer;
0060: import org.vfny.geoserver.wms.WMSMapContext;
0061: import org.vfny.geoserver.wms.responses.featureInfo.FeatureTemplate;
0062: import org.vfny.geoserver.wms.responses.featureInfo.FeatureTimeTemplate;
0063: import org.vfny.geoserver.wms.responses.map.kml.KMLGeometryTransformer.KMLGeometryTranslator;
0064: import org.xml.sax.ContentHandler;
0065: import org.xml.sax.SAXException;
0066:
0067: import java.awt.Color;
0068: import java.io.File;
0069: import java.io.IOException;
0070: import java.net.MalformedURLException;
0071: import java.net.URL;
0072: import java.text.ParseException;
0073: import java.text.SimpleDateFormat;
0074: import java.util.ArrayList;
0075: import java.util.Arrays;
0076: import java.util.Calendar;
0077: import java.util.Date;
0078: import java.util.Iterator;
0079: import java.util.List;
0080: import java.util.logging.Level;
0081: import java.util.logging.Logger;
0082:
0083: /**
0084: * Transforms a feature collection to a kml document consisting of nested
0085: * "Style" and "Placemark" elements for each feature in the collection.
0086: * A new transfomer must be instantianted for each feature collection,
0087: * the feature collection provided to the translator is supposed to be
0088: * the one coming out of the MapLayer
0089: * <p>
0090: * Usage:
0091: * </p>
0092: * @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org
0093: *
0094: */
0095: public class KMLVectorTransformer extends KMLTransformerBase {
0096: /**
0097: * logger
0098: */
0099: static Logger LOGGER = org.geotools.util.logging.Logging
0100: .getLogger("org.geoserver.kml");
0101:
0102: /**
0103: * Tolerance used to compare doubles for equality
0104: */
0105: static final double TOLERANCE = 1e-6;
0106:
0107: /**
0108: * The map context
0109: */
0110: protected final WMSMapContext mapContext;
0111:
0112: /**
0113: * The map layer being transformed
0114: */
0115: protected final MapLayer mapLayer;
0116:
0117: /**
0118: * The scale denominator.
0119: *
0120: * TODO: calcuate a real value based on image size to bbox ratio, as image
0121: * size has no meanining for KML yet this is a fudge.
0122: */
0123: double scaleDenominator = 1;
0124: NumberRange scaleRange = new NumberRange(scaleDenominator,
0125: scaleDenominator);
0126:
0127: /**
0128: * used to create 2d style objects for features
0129: */
0130: SLDStyleFactory styleFactory = new SLDStyleFactory();
0131:
0132: /**
0133: * Feature template, cached for performance reasons
0134: */
0135: FeatureTemplate template = new FeatureTemplate();
0136:
0137: /**
0138: * list of formats which correspond to the default formats in which
0139: * freemarker outputs dates when a user calls the ?datetime(),?date(),?time()
0140: * fuctions.
0141: */
0142: static List/*<SimpleDateFormat>*/dtformats = new ArrayList();
0143: static List/*<SimpleDateFormat>*/dformats = new ArrayList();
0144: static List/*<SimpleDateFormat>*/tformats = new ArrayList();
0145: static {
0146:
0147: //add default freemarker ones first since they are likely to be used
0148: // first, the order of this list matters.
0149:
0150: dtformats.add(FeatureTemplate.DATETIME_FORMAT);
0151: addFormats(dtformats, "dd%MM%yy hh:mm:ss");
0152: addFormats(dtformats, "MM%dd%yy hh:mm:ss");
0153: //addFormats(formats,"yy%MM%dd hh:mm:ss" );
0154: addFormats(dtformats, "dd%MMM%yy hh:mm:ss");
0155: addFormats(dtformats, "MMM%dd%yy hh:mm:ss");
0156: //addFormats(formats,"yy%MMM%dd hh:mm:ss" );
0157:
0158: addFormats(dtformats, "dd%MM%yy hh:mm");
0159: addFormats(dtformats, "MM%dd%yy hh:mm");
0160: //addFormats(formats,"yy%MM%dd hh:mm" );
0161: addFormats(dtformats, "dd%MMM%yy hh:mm");
0162: addFormats(dtformats, "MMM%dd%yy hh:mm");
0163: //addFormats(formats,"yy%MMM%dd hh:mm" );
0164:
0165: dformats.add(FeatureTemplate.DATE_FORMAT);
0166: addFormats(dformats, "dd%MM%yy");
0167: addFormats(dformats, "MM%dd%yy");
0168: //addFormats(formats,"yy%MM%dd" );
0169: addFormats(dformats, "dd%MMM%yy");
0170: addFormats(dformats, "MMM%dd%yy");
0171: //addFormats(formats,"yy%MMM%dd" );
0172:
0173: tformats.add(FeatureTemplate.TIME_FORMAT);
0174: }
0175:
0176: static void addFormats(List formats, String pattern) {
0177:
0178: formats.add(new SimpleDateFormat(pattern.replaceAll("%", "-")));
0179: formats.add(new SimpleDateFormat(pattern.replaceAll("%", "/")));
0180: formats.add(new SimpleDateFormat(pattern.replaceAll("%", ".")));
0181: formats.add(new SimpleDateFormat(pattern.replaceAll("%", " ")));
0182: formats.add(new SimpleDateFormat(pattern.replaceAll("%", ",")));
0183:
0184: }
0185:
0186: public KMLVectorTransformer(WMSMapContext mapContext,
0187: MapLayer mapLayer) {
0188: this .mapContext = mapContext;
0189: this .mapLayer = mapLayer;
0190:
0191: setNamespaceDeclarationEnabled(false);
0192: }
0193:
0194: /**
0195: * Sets the scale denominator.
0196: */
0197: public void setScaleDenominator(double scaleDenominator) {
0198: this .scaleDenominator = scaleDenominator;
0199: }
0200:
0201: public Translator createTranslator(ContentHandler handler) {
0202: return new KMLTranslator(handler);
0203: }
0204:
0205: protected class KMLTranslator extends KMLTranslatorSupport {
0206: /**
0207: * Geometry transformer
0208: */
0209: Translator geometryTranslator;
0210:
0211: public KMLTranslator(ContentHandler contentHandler) {
0212: super (contentHandler);
0213:
0214: KMLGeometryTransformer geometryTransformer = new KMLGeometryTransformer();
0215: //geometryTransformer.setUseDummyZ( true );
0216: geometryTransformer.setOmitXMLDeclaration(true);
0217: geometryTransformer.setNamespaceDeclarationEnabled(true);
0218:
0219: GeoServer config = mapContext.getRequest().getGeoServer();
0220: geometryTransformer.setNumDecimals(config.getNumDecimals());
0221:
0222: geometryTranslator = geometryTransformer
0223: .createTranslator(contentHandler);
0224: }
0225:
0226: public void encode(Object o) throws IllegalArgumentException {
0227: FeatureCollection features = (FeatureCollection) o;
0228: FeatureType featureType = features.getSchema();
0229:
0230: if (isStandAlone()) {
0231: start("kml");
0232: }
0233:
0234: //start the root document, name it the name of the layer
0235: start("Document");
0236: element("name", mapLayer.getTitle());
0237:
0238: //get the styles for hte layer
0239: FeatureTypeStyle[] featureTypeStyles = filterFeatureTypeStyles(
0240: mapLayer.getStyle(), featureType);
0241:
0242: // encode the schemas (kml 2.2)
0243: encodeSchemas(features);
0244:
0245: // encode the layers
0246: encode(features, featureTypeStyles);
0247:
0248: //encode the legend
0249: //encodeLegendScreenOverlay();
0250: end("Document");
0251:
0252: if (isStandAlone()) {
0253: end("kml");
0254: }
0255: }
0256:
0257: /**
0258: * Encodes the <Schema> element in kml 2.2
0259: * @param featureTypeStyles
0260: */
0261: protected void encodeSchemas(FeatureCollection featureTypeStyles) {
0262: // the code is at the moment in KML3VectorTransformer
0263: }
0264:
0265: protected void encode(FeatureCollection features,
0266: FeatureTypeStyle[] styles) {
0267: //grab a feader and process
0268: FeatureIterator reader = features.features();
0269:
0270: try {
0271: while (reader.hasNext()) {
0272: Feature feature = (Feature) reader.next();
0273:
0274: try {
0275: encode(feature, styles);
0276: } catch (RuntimeException t) {
0277: // if the stream has been closed by the client don't keep on going forward, this is not
0278: // a feature local issue
0279: if (t.getCause() instanceof SAXException)
0280: throw t;
0281: else
0282: LOGGER.log(Level.WARNING,
0283: "Failure tranforming feature to KML:"
0284: + feature.getID(), t);
0285: }
0286: }
0287: } finally {
0288: //make sure we always close
0289: features.close(reader);
0290: }
0291: }
0292:
0293: protected void encode(Feature feature, FeatureTypeStyle[] styles) {
0294: //get the feature id
0295: String featureId = featureId(feature);
0296:
0297: //start the document
0298: //start("Document");
0299:
0300: // element("name", featureId);
0301: // element("title", mapLayer.getTitle());
0302:
0303: //encode the styles, keep track of any labels provided by the
0304: // styles
0305: if (encodeStyle(feature, styles)) {
0306: encodePlacemark(feature, styles);
0307: }
0308:
0309: //end("Document");
0310: }
0311:
0312: /**
0313: * Encodes the provided set of rules as KML styles.
0314: */
0315: protected boolean encodeStyle(Feature feature,
0316: FeatureTypeStyle[] styles) {
0317:
0318: //encode hte Line/Poly styles
0319: List symbolizerList = new ArrayList();
0320: for (int j = 0; j < styles.length; j++) {
0321: Rule[] rules = filterRules(styles[j], feature);
0322:
0323: for (int i = 0; i < rules.length; i++) {
0324: symbolizerList.addAll(Arrays.asList(rules[i]
0325: .getSymbolizers()));
0326: }
0327: }
0328:
0329: if (!symbolizerList.isEmpty()) {
0330: //start the style
0331: start("Style", KMLUtils.attributes(new String[] { "id",
0332: "GeoServerStyle" + feature.getID() }));
0333:
0334: //encode the icon
0335: encodeIconStyle(feature, styles);
0336:
0337: Symbolizer[] symbolizers = (Symbolizer[]) symbolizerList
0338: .toArray(new Symbolizer[symbolizerList.size()]);
0339: encodeStyle(feature, symbolizers);
0340:
0341: //end the style
0342: end("Style");
0343:
0344: //return true to specify that the feature has a style
0345: return true;
0346: } else {
0347: //dont encode
0348: return false;
0349: }
0350:
0351: }
0352:
0353: /**
0354: * Encodes an IconStyle for a feature.
0355: */
0356: protected void encodeIconStyle(Feature feature,
0357: FeatureTypeStyle[] styles) {
0358: //encode the style for the icon
0359: //start IconStyle
0360: start("IconStyle");
0361:
0362: //make transparent if they didn't ask for attributes
0363: if (!mapContext.getRequest().getKMattr()) {
0364: encodeColor("00ffffff");
0365: }
0366:
0367: //figure out if line or polygon
0368: boolean line = feature.getDefaultGeometry() != null
0369: && (feature.getDefaultGeometry() instanceof LineString || feature
0370: .getDefaultGeometry() instanceof MultiLineString);
0371: boolean poly = feature.getDefaultGeometry() != null
0372: && (feature.getDefaultGeometry() instanceof Polygon || feature
0373: .getDefaultGeometry() instanceof MultiPolygon);
0374:
0375: //if line or polygon scale the label
0376: if (line || poly) {
0377: element("scale", "0.4");
0378: }
0379: //start Icon
0380: start("Icon");
0381:
0382: if (line || poly) {
0383: String imageURL;
0384: try {
0385: URL requestURL = new URL(mapContext.getRequest()
0386: .getBaseUrl());
0387: imageURL = requestURL.getProtocol() + "://"
0388: + requestURL.getHost() + ":"
0389: + requestURL.getPort();
0390: imageURL += "/geoserver/"
0391: + (poly ? "icon-poly.png" : "icon-line.png");
0392: } catch (MalformedURLException mue) {
0393: imageURL = "http://maps.google.com/mapfiles/kml/pal3/icon61.png";
0394: }
0395: element("href", imageURL);
0396: } else {
0397: //do nothing, this is handled by encodePointStyle
0398: }
0399:
0400: end("Icon");
0401:
0402: //end IconStyle
0403: end("IconStyle");
0404:
0405: }
0406:
0407: /**
0408: * Encodes the provided set of symbolizers as KML styles.
0409: */
0410: protected void encodeStyle(Feature feature,
0411: Symbolizer[] symbolizers) {
0412: // look for line symbolizers, if there is any, we should tell the
0413: // polygon style to have an outline
0414: boolean forceOutline = false;
0415: for (int i = 0; i < symbolizers.length; i++) {
0416: if (symbolizers[i] instanceof LineSymbolizer) {
0417: forceOutline = true;
0418: break;
0419: }
0420: }
0421:
0422: for (int i = 0; i < symbolizers.length; i++) {
0423: Symbolizer symbolizer = symbolizers[i];
0424: LOGGER.finer(new StringBuffer("Applying symbolizer ")
0425: .append(symbolizer).toString());
0426:
0427: //create a 2-D style
0428: Style2D style = styleFactory.createStyle(feature,
0429: symbolizer, scaleRange);
0430:
0431: //split out each type of symbolizer
0432: if (symbolizer instanceof TextSymbolizer) {
0433: encodeTextStyle((TextStyle2D) style,
0434: (TextSymbolizer) symbolizer);
0435: }
0436:
0437: if (symbolizer instanceof PolygonSymbolizer) {
0438: encodePolygonStyle((PolygonStyle2D) style,
0439: (PolygonSymbolizer) symbolizer,
0440: forceOutline);
0441: }
0442:
0443: if (symbolizer instanceof LineSymbolizer) {
0444: encodeLineStyle((LineStyle2D) style,
0445: (LineSymbolizer) symbolizer);
0446: }
0447:
0448: if (symbolizer instanceof PointSymbolizer) {
0449: encodePointStyle(style,
0450: (PointSymbolizer) symbolizer);
0451: }
0452: }
0453: }
0454:
0455: /**
0456: * Encodes a KML IconStyle + PolyStyle from a polygon style and symbolizer.
0457: */
0458: protected void encodePolygonStyle(PolygonStyle2D style,
0459: PolygonSymbolizer symbolizer, boolean forceOutline) {
0460: //star the polygon style
0461: start("PolyStyle");
0462:
0463: //fill
0464: if (symbolizer.getFill() != null) {
0465: //get opacity
0466: double opacity = SLD.opacity(symbolizer.getFill());
0467:
0468: if (Double.isNaN(opacity)) {
0469: //none specified, default to full opacity
0470: opacity = 1.0;
0471: }
0472:
0473: encodeColor(SLD.color(symbolizer.getFill()), opacity);
0474: } else {
0475: //make it transparent
0476: encodeColor("00aaaaaa");
0477: }
0478:
0479: //outline
0480: if (symbolizer.getStroke() != null || forceOutline) {
0481: element("outline", "1");
0482: } else {
0483: element("outline", "0");
0484: }
0485:
0486: end("PolyStyle");
0487:
0488: //if stroke specified add line style as well
0489: if (symbolizer.getStroke() != null) {
0490: start("LineStyle");
0491:
0492: //opacity
0493: double opacity = SLD.opacity(symbolizer.getStroke());
0494:
0495: if (Double.isNaN(opacity)) {
0496: //none specified, default to full opacity
0497: opacity = 1.0;
0498: }
0499:
0500: encodeColor(colorToHex((Color) style.getContour(),
0501: opacity));
0502:
0503: //width
0504: int width = SLD.width(symbolizer.getStroke());
0505:
0506: if (width != SLD.NOTFOUND) {
0507: element("width", Integer.toString(width));
0508: }
0509:
0510: end("LineStyle");
0511: }
0512: }
0513:
0514: /**
0515: * Encodes a KML IconStyle + LineStyle from a polygon style and symbolizer.
0516: */
0517: protected void encodeLineStyle(LineStyle2D style,
0518: LineSymbolizer symbolizer) {
0519: start("LineStyle");
0520:
0521: //stroke
0522: if (symbolizer.getStroke() != null) {
0523: //opacity
0524: double opacity = SLD.opacity(symbolizer.getStroke());
0525:
0526: if (Double.isNaN(opacity)) {
0527: //default to full opacity
0528: opacity = 1.0;
0529: }
0530:
0531: encodeColor((Color) style.getContour(), opacity);
0532:
0533: //width
0534: int width = SLD.width(symbolizer.getStroke());
0535:
0536: if (width != SLD.NOTFOUND) {
0537: element("width", Integer.toString(width));
0538: }
0539: } else {
0540: //default
0541: encodeColor("ffaaaaaa");
0542: element("width", "1");
0543: }
0544:
0545: end("LineStyle");
0546: }
0547:
0548: /**
0549: * Encodes a KML IconStyle from a point style and symbolizer.
0550: */
0551: protected void encodePointStyle(Style2D style,
0552: PointSymbolizer symbolizer) {
0553: start("IconStyle");
0554:
0555: if (style instanceof MarkStyle2D) {
0556: Mark mark = SLD.mark(symbolizer);
0557:
0558: if (mark != null) {
0559: double opacity = SLD.opacity(mark.getFill());
0560:
0561: if (Double.isNaN(opacity)) {
0562: //default to full opacity
0563: opacity = 1.0;
0564: }
0565:
0566: if (mark.getFill() != null) {
0567: final Color color = SLD.color(mark.getFill());
0568: encodeColor(color, opacity);
0569: }
0570: } else {
0571: //default
0572: encodeColor("ffaaaaaa");
0573: }
0574: } else {
0575: //default
0576: encodeColor("ffaaaaaa");
0577: }
0578:
0579: element("colorMode", "normal");
0580:
0581: // placemark icon
0582: String iconHref = null;
0583:
0584: //if the point symbolizer uses an external graphic use it
0585: if ((symbolizer.getGraphic() != null)
0586: && (symbolizer.getGraphic().getExternalGraphics() != null)
0587: && (symbolizer.getGraphic().getExternalGraphics().length > 0)) {
0588: ExternalGraphic graphic = symbolizer.getGraphic()
0589: .getExternalGraphics()[0];
0590:
0591: try {
0592: if ("file".equals(graphic.getLocation()
0593: .getProtocol())) {
0594: //it is a local file, reference locally from "styles" directory
0595: File file = new File(graphic.getLocation()
0596: .getFile());
0597: iconHref = RequestUtils.baseURL(mapContext
0598: .getRequest().getHttpServletRequest())
0599: + "styles/" + file.getName();
0600: } else if ("http".equals(graphic.getLocation()
0601: .getProtocol())) {
0602: iconHref = graphic.getLocation().toString();
0603: } else {
0604: // TODO: should we check for http:// and use it directly?
0605: //other protocols?
0606: }
0607:
0608: } catch (Exception e) {
0609: LOGGER.log(Level.WARNING,
0610: "Error processing external graphic:"
0611: + graphic, e);
0612: }
0613: }
0614:
0615: if (iconHref == null) {
0616: iconHref = "http://maps.google.com/mapfiles/kml/pal4/icon25.png";
0617: }
0618:
0619: start("Icon");
0620:
0621: element("href", iconHref);
0622: end("Icon");
0623:
0624: end("IconStyle");
0625: }
0626:
0627: /**
0628: * Encodes a KML LabelStyle from a text style and symbolizer.
0629: */
0630: protected void encodeTextStyle(TextStyle2D style,
0631: TextSymbolizer symbolizer) {
0632: start("LabelStyle");
0633:
0634: if (symbolizer.getFill() != null) {
0635: double opacity = SLD.opacity(symbolizer.getFill());
0636:
0637: if (Double.isNaN(opacity)) {
0638: //default to full opacity
0639: opacity = 1.0;
0640: }
0641:
0642: encodeColor(SLD.color(symbolizer.getFill()), opacity);
0643: } else {
0644: //default
0645: encodeColor("ffffffff");
0646: }
0647:
0648: end("LabelStyle");
0649: }
0650:
0651: /**
0652: * Encodes a KML Placemark from a feature and optional name.
0653: */
0654: protected void encodePlacemark(Feature feature,
0655: FeatureTypeStyle[] styles) {
0656: Geometry geometry = featureGeometry(feature);
0657: Coordinate centroid = geometryCentroid(geometry);
0658:
0659: start("Placemark", KMLUtils.attributes(new String[] { "id",
0660: feature.getID() }));
0661:
0662: //encode name + description only if kmattr was specified
0663: if (mapContext.getRequest().getKMattr()) {
0664: //name
0665: try {
0666: encodePlacemarkName(feature, styles);
0667: } catch (Exception e) {
0668: String msg = "Error occured processing 'title' template.";
0669: LOGGER.log(Level.WARNING, msg, e);
0670: }
0671:
0672: //description
0673: try {
0674: encodePlacemarkDescription(feature);
0675: } catch (Exception e) {
0676: String msg = "Error occured processing 'description' template.";
0677: LOGGER.log(Level.WARNING, msg, e);
0678: }
0679: }
0680:
0681: //look at
0682: encodePlacemarkLookAt(centroid);
0683:
0684: //time
0685: try {
0686: encodePlacemarkTime(feature);
0687: } catch (Exception e) {
0688: String msg = "Error occured processing 'time' template: "
0689: + e.getMessage();
0690: LOGGER.log(Level.WARNING, msg);
0691: LOGGER.log(Level.FINE, "", e);
0692: }
0693:
0694: //style reference
0695: element("styleUrl", "#GeoServerStyle" + feature.getID());
0696:
0697: // encode extended data (kml 2.2)
0698: encodeExtendedData(feature);
0699:
0700: //geometry
0701: encodePlacemarkGeometry(geometry, centroid);
0702:
0703: end("Placemark");
0704: }
0705:
0706: /**
0707: * Encodes kml 2.2 extended data section
0708: * @param feature
0709: */
0710: protected void encodeExtendedData(Feature feature) {
0711: // code at the moment is in KML3VectorTransfomer
0712: }
0713:
0714: /**
0715: * Encodes a KML Placemark name from a feature by processing a
0716: * template.
0717: */
0718: protected void encodePlacemarkName(Feature feature,
0719: FeatureTypeStyle[] styles) throws IOException {
0720:
0721: //order to use when figuring out what the name / label of a
0722: // placemark should be:
0723: // 1. the title template for features
0724: // 2. a text sym with a label from teh sld
0725: // 3. nothing ( do not use fid )
0726:
0727: String title = template.title(feature);
0728:
0729: //ensure not empty and != fid
0730: if (title == null || "".equals(title)
0731: || feature.getID().equals(title)) {
0732: //try sld
0733: StringBuffer label = new StringBuffer();
0734: for (int i = 0; i < styles.length; i++) {
0735: Rule[] rules = filterRules(styles[i], feature);
0736: for (int j = 0; j < rules.length; j++) {
0737: Symbolizer[] syms = rules[j].getSymbolizers();
0738: for (int k = 0; k < syms.length; k++) {
0739: if (syms[k] instanceof TextSymbolizer) {
0740: Expression e = SLD
0741: .textLabel((TextSymbolizer) syms[k]);
0742: Object object = e.evaluate(feature);
0743: String value = null;
0744:
0745: if (object instanceof String) {
0746: value = (String) object;
0747: } else {
0748: if (object != null) {
0749: value = object.toString();
0750: }
0751: }
0752:
0753: if ((value != null)
0754: && !"".equals(value.trim())) {
0755: label.append(value);
0756: }
0757: }
0758: }
0759: }
0760: }
0761:
0762: if (label.length() > 0) {
0763: title = label.toString();
0764: } else {
0765: title = null;
0766: }
0767:
0768: }
0769:
0770: if (title != null) {
0771: start("name");
0772: cdata(title);
0773: end("name");
0774: }
0775:
0776: }
0777:
0778: /**
0779: * Encodes a KML Placemark description from a feature by processing a
0780: * template.
0781: */
0782: protected void encodePlacemarkDescription(Feature feature)
0783: throws IOException {
0784:
0785: String description = template.description(feature);
0786:
0787: if (description != null) {
0788: start("description");
0789: cdata(description);
0790: end("description");
0791: }
0792: }
0793:
0794: /**
0795: * Encods a KML Placemark LookAt from a geometry + centroid.
0796: */
0797: protected void encodePlacemarkLookAt(Coordinate centroid) {
0798: start("LookAt");
0799:
0800: element("longitude", Double.toString(centroid.x));
0801: element("latitude", Double.toString(centroid.y));
0802: element("range", "700");
0803: element("tilt", "10.0");
0804: element("heading", "10.0");
0805:
0806: end("LookAt");
0807: }
0808:
0809: /**
0810: * Encodes a KML TimePrimitive geometry from a feature.
0811: */
0812: protected void encodePlacemarkTime(Feature feature)
0813: throws IOException {
0814: try {
0815: String[] time = new FeatureTimeTemplate(template)
0816: .execute(feature);
0817: if (time.length == 0) {
0818: return;
0819: }
0820:
0821: if (time.length == 1) {
0822: String datetime = encodeDateTime(time[0]);
0823: if (datetime != null) {
0824: //timestamp case
0825: start("TimeStamp");
0826: element("when", datetime);
0827: end("TimeStamp");
0828: }
0829:
0830: } else {
0831: //timespan case
0832: String begin = encodeDateTime(time[0]);
0833: String end = encodeDateTime(time[1]);
0834:
0835: if (!(begin == null && end == null)) {
0836: start("TimeSpan");
0837: if (begin != null) {
0838: element("begin", begin);
0839: }
0840: if (end != null) {
0841: element("end", end);
0842: }
0843: end("TimeSpan");
0844: }
0845: }
0846: } catch (Exception e) {
0847: throw (IOException) new IOException().initCause(e);
0848: }
0849: }
0850:
0851: /**
0852: * Encodes a date as an xs:dateTime.
0853: */
0854: protected String encodeDateTime(String date) throws Exception {
0855:
0856: //first try as date time
0857: Date d = parseDate(dtformats, date);
0858: if (d == null) {
0859: //then try as date
0860: d = parseDate(dformats, date);
0861: }
0862: if (d == null) {
0863: //try as time
0864: d = parseDate(tformats, date);
0865: }
0866:
0867: if (d == null) {
0868: //last ditch effort, try to parse as xml dates
0869: try {
0870: //try as xml date time
0871: d = DateUtil.deserializeDateTime(date);
0872: } catch (Exception e1) {
0873: try {
0874: //try as xml date
0875: d = DateUtil.deserializeDate(date);
0876: } catch (Exception e2) {
0877: }
0878: }
0879: }
0880:
0881: if (d != null) {
0882: Calendar c = Calendar.getInstance();
0883: c.setTime(d);
0884: return new XSDateTimeBinding().encode(c, null);
0885: }
0886:
0887: LOGGER.warning("Could not parse date: " + date);
0888: return null;
0889: }
0890:
0891: /**
0892: * Parses a date as a string into a well-known format.
0893: */
0894: protected Date parseDate(List formats, String date) {
0895: for (Iterator f = formats.iterator(); f.hasNext();) {
0896: SimpleDateFormat format = (SimpleDateFormat) f.next();
0897: Date d = null;
0898: try {
0899: d = format.parse(date);
0900: } catch (ParseException e) {
0901: }
0902:
0903: if (d != null) {
0904: return d;
0905: }
0906: }
0907:
0908: return null;
0909: }
0910:
0911: /**
0912: * Encodes a KML Placemark geometry from a geometry + centroid.
0913: */
0914: protected void encodePlacemarkGeometry(Geometry geometry,
0915: Coordinate centroid) {
0916: //if point, just encode a single point, otherwise encode the geometry
0917: // + centroid
0918: if (geometry instanceof Point
0919: || (geometry instanceof MultiPoint)
0920: && ((MultiPoint) geometry).getNumPoints() == 1) {
0921: encodeGeometry(geometry);
0922: } else {
0923: start("MultiGeometry");
0924:
0925: //the centroid
0926: start("Point");
0927:
0928: if (!Double.isNaN(centroid.z)) {
0929: element("coordinates", centroid.x + ","
0930: + centroid.y + "," + centroid.z);
0931: } else {
0932: element("coordinates", centroid.x + ","
0933: + centroid.y);
0934: }
0935:
0936: end("Point");
0937:
0938: //the actual geometry
0939: encodeGeometry(geometry);
0940:
0941: end("MultiGeometry");
0942: }
0943:
0944: }
0945:
0946: /**
0947: * Encodes a KML geometry.
0948: */
0949: protected void encodeGeometry(Geometry geometry) {
0950: if (geometry instanceof GeometryCollection) {
0951: //unwrap the collection
0952: GeometryCollection collection = (GeometryCollection) geometry;
0953:
0954: for (int i = 0; i < collection.getNumGeometries(); i++) {
0955: encodeGeometry(collection.getGeometryN(i));
0956: }
0957: } else {
0958: geometryTranslator.encode(geometry);
0959: }
0960: }
0961:
0962: /**
0963: * Encodes a color element from its color + opacity representation.
0964: *
0965: * @param color The color to encode.
0966: * @param opacity The opacity ( alpha ) of the color.
0967: */
0968: void encodeColor(Color color, double opacity) {
0969: encodeColor(colorToHex(color, opacity));
0970: }
0971:
0972: /**
0973: * Encodes a color element from its hex representation.
0974: *
0975: * @param hex The hex value ( with alpha ) of the color.
0976: *
0977: */
0978: void encodeColor(String hex) {
0979: element("color", hex);
0980: }
0981:
0982: /**
0983: * Checks if a rule can be triggered at the current scale level
0984: *
0985: * @param r
0986: * The rule
0987: * @return true if the scale is compatible with the rule settings
0988: */
0989: boolean isWithInScale(Rule r) {
0990: return ((r.getMinScaleDenominator() - TOLERANCE) <= scaleDenominator)
0991: && ((r.getMaxScaleDenominator() + TOLERANCE) > scaleDenominator);
0992: }
0993:
0994: /**
0995: * Returns the id of the feature removing special characters like
0996: * '&','>','<','%'.
0997: */
0998: String featureId(Feature feature) {
0999: String id = feature.getID();
1000: id = id.replaceAll("&", "");
1001: id = id.replaceAll(">", "");
1002: id = id.replaceAll("<", "");
1003: id = id.replaceAll("%", "");
1004:
1005: return id;
1006: }
1007:
1008: /**
1009: * Rreturns the geometry for the feature reprojecting if necessary.
1010: */
1011: Geometry featureGeometry(Feature f) {
1012: // get the geometry
1013: Geometry geom = f.getDefaultGeometry();
1014:
1015: //rprojection done in KMLTransformer
1016: // if (!CRS.equalsIgnoreMetadata(sourceCrs, mapContext.getCoordinateReferenceSystem())) {
1017: // try {
1018: // MathTransform transform = CRS.findMathTransform(sourceCrs,
1019: // mapContext.getCoordinateReferenceSystem(), true);
1020: // geom = JTS.transform(geom, transform);
1021: // } catch (MismatchedDimensionException e) {
1022: // LOGGER.severe(e.getLocalizedMessage());
1023: // } catch (TransformException e) {
1024: // LOGGER.severe(e.getLocalizedMessage());
1025: // } catch (FactoryException e) {
1026: // LOGGER.severe(e.getLocalizedMessage());
1027: // }
1028: // }
1029:
1030: return geom;
1031: }
1032:
1033: /**
1034: * Returns the centroid of the geometry, handling a geometry collection.
1035: * <p>
1036: * In the case of a collection a multi point containing the centroid of
1037: * each geometry in the collection is calculated. The first point in
1038: * the multi point is returned as the cetnroid.
1039: * </p>
1040: */
1041: Coordinate geometryCentroid(Geometry g) {
1042: //TODO: should the collecftion case return the centroid of hte
1043: // multi point?
1044: if (g instanceof GeometryCollection) {
1045: GeometryCollection gc = (GeometryCollection) g;
1046:
1047: //check for case of single geometry
1048: if (gc.getNumGeometries() == 1) {
1049: g = gc.getGeometryN(0);
1050: } else {
1051: Coordinate[] pts = new Coordinate[gc
1052: .getNumGeometries()];
1053:
1054: for (int t = 0; t < gc.getNumGeometries(); t++) {
1055: pts[t] = gc.getGeometryN(t).getCentroid()
1056: .getCoordinate();
1057: }
1058:
1059: return g.getFactory().createMultiPoint(pts)
1060: .getCoordinates()[0];
1061: }
1062: }
1063:
1064: if (g instanceof Point) {
1065: //thats easy
1066: return g.getCoordinate();
1067: } else if (g instanceof LineString) {
1068: //make sure the point we return is actually on the line
1069: double tol = 1E-6;
1070: double mid = g.getLength() / 2d;
1071:
1072: Coordinate[] coords = g.getCoordinates();
1073:
1074: //walk along the linestring until we get to a point where we
1075: // have two coordinates that straddle the midpoint
1076: double len = 0d;
1077: for (int i = 1; i < coords.length; i++) {
1078: LineSegment line = new LineSegment(coords[i - 1],
1079: coords[i]);
1080: len += line.getLength();
1081:
1082: if (Math.abs(len - mid) < tol) {
1083: //close enough
1084: return line.getCoordinate(1);
1085: }
1086:
1087: if (len > mid) {
1088: //we have gone past midpoint
1089: return line.pointAlong(1 - ((len - mid) / line
1090: .getLength()));
1091: }
1092: }
1093:
1094: //should never get there
1095: return g.getCentroid().getCoordinate();
1096: } else {
1097: //return the actual centroid
1098: return g.getCentroid().getCoordinate();
1099: }
1100: }
1101:
1102: /**
1103: * Utility method to convert an int into hex, padded to two characters.
1104: * handy for generating colour strings.
1105: *
1106: * @param i Int to convert
1107: * @return String a two character hex representation of i
1108: * NOTE: this is a utility method and should be put somewhere more useful.
1109: */
1110: String intToHex(int i) {
1111: String prelim = Integer.toHexString(i);
1112:
1113: if (prelim.length() < 2) {
1114: prelim = "0" + prelim;
1115: }
1116:
1117: return prelim;
1118: }
1119:
1120: /**
1121: * Utility method to convert a Color and opacity (0,1.0) into a KML
1122: * color ref.
1123: *
1124: * @param c The color to convert.
1125: * @param opacity Opacity / alpha, double from 0 to 1.0.
1126: *
1127: * @return A String of the form "#AABBGGRR".
1128: */
1129: String colorToHex(Color c, double opacity) {
1130: return new StringBuffer().append(
1131: intToHex(new Float(255 * opacity).intValue()))
1132: .append(intToHex(c.getBlue())).append(
1133: intToHex(c.getGreen())).append(
1134: intToHex(c.getRed())).toString();
1135: }
1136:
1137: /**
1138: * Filters the feature type styles of <code>style</code> returning only
1139: * those that apply to <code>featureType</code>
1140: * <p>
1141: * This methods returns feature types for which
1142: * <code>featureTypeStyle.getFeatureTypeName()</code> matches the name
1143: * of the feature type of <code>featureType</code>, or matches the name of
1144: * any parent type of the feature type of <code>featureType</code>. This
1145: * method returns an empty array in the case of which no rules match.
1146: * </p>
1147: * @param style The style containing the feature type styles.
1148: * @param featureType The feature type being filtered against.
1149: *
1150: */
1151: protected FeatureTypeStyle[] filterFeatureTypeStyles(
1152: Style style, FeatureType featureType) {
1153: FeatureTypeStyle[] featureTypeStyles = style
1154: .getFeatureTypeStyles();
1155:
1156: if ((featureTypeStyles == null)
1157: || (featureTypeStyles.length == 0)) {
1158: return new FeatureTypeStyle[0];
1159: }
1160:
1161: ArrayList filtered = new ArrayList(featureTypeStyles.length);
1162:
1163: for (int i = 0; i < featureTypeStyles.length; i++) {
1164: FeatureTypeStyle featureTypeStyle = featureTypeStyles[i];
1165: String featureTypeName = featureTypeStyle
1166: .getFeatureTypeName();
1167:
1168: //does this style have any rules
1169: if (featureTypeStyle.getRules() == null
1170: || featureTypeStyle.getRules().length == 0) {
1171: continue;
1172: }
1173:
1174: //does this style apply to the feature collection
1175: if (featureType.getTypeName().equalsIgnoreCase(
1176: featureTypeName)
1177: || featureType.isDescendedFrom(null,
1178: featureTypeName)) {
1179: filtered.add(featureTypeStyle);
1180: }
1181: }
1182:
1183: return (FeatureTypeStyle[]) filtered
1184: .toArray(new FeatureTypeStyle[filtered.size()]);
1185: }
1186:
1187: /**
1188: * Filters the rules of <code>featureTypeStyle</code> returnting only
1189: * those that apply to <code>feature</code>.
1190: * <p>
1191: * This method returns rules for which:
1192: * <ol>
1193: * <li><code>rule.getFilter()</code> matches <code>feature</code>, or:
1194: * <li>the rule defines an "ElseFilter", and the feature matches no
1195: * other rules.
1196: * </ol>
1197: * This method returns an empty array in the case of which no rules
1198: * match.
1199: * </p>
1200: * @param featureTypeStyle The feature type style containing the rules.
1201: * @param feature The feature being filtered against.
1202: *
1203: */
1204: Rule[] filterRules(FeatureTypeStyle featureTypeStyle,
1205: Feature feature) {
1206: Rule[] rules = featureTypeStyle.getRules();
1207:
1208: if ((rules == null) || (rules.length == 0)) {
1209: return new Rule[0];
1210: }
1211:
1212: ArrayList filtered = new ArrayList(rules.length);
1213:
1214: //process the rules, keep track of the need to apply an else filters
1215: boolean match = false;
1216: boolean hasElseFilter = false;
1217:
1218: for (int i = 0; i < rules.length; i++) {
1219: Rule rule = rules[i];
1220: LOGGER.finer(new StringBuffer("Applying rule: ")
1221: .append(rule.toString()).toString());
1222:
1223: //does this rule have an else filter
1224: if (rule.hasElseFilter()) {
1225: hasElseFilter = true;
1226:
1227: continue;
1228: }
1229:
1230: //is this rule within scale?
1231: if (!isWithInScale(rule)) {
1232: continue;
1233: }
1234:
1235: //does this rule have a filter which applies to the feature
1236: Filter filter = rule.getFilter();
1237:
1238: if ((filter == null) || filter.evaluate(feature)) {
1239: match = true;
1240:
1241: filtered.add(rule);
1242: }
1243: }
1244:
1245: //if no rules mached the feautre, re-run through the rules applying
1246: // any else filters
1247: if (!match && hasElseFilter) {
1248: //loop through again and apply all the else rules
1249: for (int i = 0; i < rules.length; i++) {
1250: Rule rule = rules[i];
1251:
1252: //is this rule within scale?
1253: if (!isWithInScale(rule)) {
1254: continue;
1255: }
1256:
1257: if (rule.hasElseFilter()) {
1258: filtered.add(rule);
1259: }
1260: }
1261: }
1262:
1263: return (Rule[]) filtered.toArray(new Rule[filtered.size()]);
1264: }
1265: }
1266: }
|