/* (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.decorator; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.kml.KmlEncodingContext; import org.geoserver.kml.utils.KmlCentroidBuilder; import org.geoserver.platform.ServiceException; import org.geoserver.wms.WMSInfo; import org.geoserver.wms.featureinfo.FeatureTemplate; import org.geotools.util.Converters; import org.geotools.util.logging.Logging; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateFilter; import com.vividsolutions.jts.geom.CoordinateSequence; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import de.micromata.opengis.kml.v_2_2_0.AltitudeMode; import de.micromata.opengis.kml.v_2_2_0.Feature; import de.micromata.opengis.kml.v_2_2_0.MultiGeometry; import de.micromata.opengis.kml.v_2_2_0.Placemark; /** * Encodes the geometry element for Placemark elements * * @author Andrea Aime - GeoSolutions * */ public class PlacemarkGeometryDecoratorFactory implements KmlDecoratorFactory { static final KmlCentroidBuilder CENTROIDS = new KmlCentroidBuilder(); @Override public KmlDecorator getDecorator(Class<? extends Feature> featureClass, KmlEncodingContext context) { if (Placemark.class.isAssignableFrom(featureClass)) { boolean hasHeightTemplate = hasHeightTemplate(context); boolean isExtrudeEnabled = isExtrudeEnabled(context); return new PlacemarkGeometryDecorator(hasHeightTemplate, isExtrudeEnabled); } else { return null; } } private boolean hasHeightTemplate(KmlEncodingContext context) { // we apply the height template only on wms outputs if(!(context.getService() instanceof WMSInfo)) { return false; } try { SimpleFeatureType schema = context.getCurrentFeatureCollection().getSchema(); return !context.getTemplate().isTemplateEmpty(schema, "height.ftl", FeatureTemplate.class, "0\n"); } catch(IOException e) { throw new ServiceException("Failed to apply height template during kml generation", e); } } private boolean isExtrudeEnabled(KmlEncodingContext context) { // extrusion applies only to WMS mode if(!(context.getService() instanceof WMSInfo)) { return false; } // were we asked not to perform extrusion? Object foExtrude = context.getRequest().getFormatOptions().get("extrude"); if(foExtrude == null) { // true by default return true; } // be careful, in case someone set extrude to some funny value Boolean extrude = Converters.convert(foExtrude, Boolean.class); return extrude == Boolean.TRUE; } static class PlacemarkGeometryDecorator implements KmlDecorator { static final Logger LOGGER = Logging.getLogger(PlacemarkGeometryDecorator.class); private boolean hasHeightTemplate; private boolean extrudeEnabled; public PlacemarkGeometryDecorator(boolean hasHeightTemplate, boolean extrudeEnabled) { this.hasHeightTemplate = hasHeightTemplate; this.extrudeEnabled = extrudeEnabled; } @Override public Feature decorate(Feature feature, KmlEncodingContext context) { // encode the geometry Placemark pm = (Placemark) feature; SimpleFeature sf = context.getCurrentFeature(); double height = Double.NaN; if(hasHeightTemplate) { try { String output = context.getTemplate().template(sf, "height.ftl", FeatureTemplate.class); height = Double.valueOf(output); } catch (IOException ioe) { LOGGER.log(Level.WARNING, "Couldn't render height template for " + sf.getID(), ioe); } } Geometry geometry = getFeatureGeometry(sf, height); if (geometry != null) { pm.setGeometry(encodeGeometry(geometry, context, height)); } return feature; } /** * Extracts the * * @param sf * @param context * */ private Geometry getFeatureGeometry(SimpleFeature sf, final double height) { Geometry geom = (Geometry) sf.getDefaultGeometry(); 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(); } return geom; } private de.micromata.opengis.kml.v_2_2_0.Geometry encodeGeometry(Geometry geometry, KmlEncodingContext context, double height) { de.micromata.opengis.kml.v_2_2_0.Geometry kmlGeometry = toKmlGeometry(geometry); boolean isSinglePoint = geometry instanceof Point || (geometry instanceof MultiPoint) && ((MultiPoint) geometry).getNumPoints() == 1; // if is not a single point and is description enabled, we // add and extrude a centroid together with the geometry if (!isSinglePoint && context.isDescriptionEnabled()) { MultiGeometry mg = new MultiGeometry(); // centroid + full geometry Coordinate c = CENTROIDS.geometryCentroid(geometry); if(!Double.isNaN(height)) { c.setOrdinate(2, height); } de.micromata.opengis.kml.v_2_2_0.Point kmlPoint = toKmlPoint(c); mg.addToGeometry(kmlPoint); // encode the full geometry mg.addToGeometry(kmlGeometry); kmlGeometry = mg; } if(hasHeightTemplate) { applyExtrusion(kmlGeometry); } return kmlGeometry; } private de.micromata.opengis.kml.v_2_2_0.Point toKmlPoint(Coordinate c) { de.micromata.opengis.kml.v_2_2_0.Point result = new de.micromata.opengis.kml.v_2_2_0.Point(); if (Double.isNaN(c.z)) { result.addToCoordinates(c.x, c.y); } else { result.addToCoordinates(c.x, c.y, c.z); } return result; } private de.micromata.opengis.kml.v_2_2_0.Geometry toKmlGeometry(Geometry geometry) { if(geometry == null) { return null; } if (geometry instanceof GeometryCollection) { MultiGeometry mg = new MultiGeometry(); GeometryCollection gc = (GeometryCollection) geometry; if(gc.getNumGeometries() == 1) { return toKmlGeometry(gc.getGeometryN(0)); } else { for (int i = 0; i < gc.getNumGeometries(); i++) { Geometry child = gc.getGeometryN(i); mg.addToGeometry(toKmlGeometry(child)); } } return mg; } else if (geometry instanceof Point) { return toKmlPoint(geometry.getCoordinate()); } else if (geometry instanceof LinearRing) { return convertLinearRing((LinearRing) geometry); } else if (geometry instanceof LineString) { de.micromata.opengis.kml.v_2_2_0.LineString kmlLine = new de.micromata.opengis.kml.v_2_2_0.LineString(); List<de.micromata.opengis.kml.v_2_2_0.Coordinate> kmlCoordinates = dumpCoordinateSequence(((LineString) geometry) .getCoordinateSequence()); kmlLine.setCoordinates(kmlCoordinates); return kmlLine; } else if (geometry instanceof Polygon) { Polygon polygon = (Polygon) geometry; de.micromata.opengis.kml.v_2_2_0.Polygon kmlPolygon = new de.micromata.opengis.kml.v_2_2_0.Polygon(); de.micromata.opengis.kml.v_2_2_0.LinearRing kmlOuterRing = convertLinearRing((LinearRing) polygon .getExteriorRing()); kmlPolygon.createAndSetOuterBoundaryIs().setLinearRing(kmlOuterRing); for (int i = 0; i < polygon.getNumInteriorRing(); i++) { LinearRing interior = (LinearRing) polygon.getInteriorRingN(i); de.micromata.opengis.kml.v_2_2_0.LinearRing kmlInterior = convertLinearRing(interior); kmlPolygon.createAndAddInnerBoundaryIs().setLinearRing(kmlInterior); } return kmlPolygon; } else { throw new IllegalArgumentException("Unrecognized geometry type: " + geometry); } } private de.micromata.opengis.kml.v_2_2_0.LinearRing convertLinearRing(LinearRing geometry) { de.micromata.opengis.kml.v_2_2_0.LinearRing kmlLine = new de.micromata.opengis.kml.v_2_2_0.LinearRing(); List<de.micromata.opengis.kml.v_2_2_0.Coordinate> kmlCoordinates = dumpCoordinateSequence(((LineString) geometry) .getCoordinateSequence()); kmlLine.setCoordinates(kmlCoordinates); if(!hasHeightTemplate) { // allow the polygon to follow the ground, otherwise some polygons with long // edges will disappear in mountain areas kmlLine.setTessellate(true); } return kmlLine; } private List<de.micromata.opengis.kml.v_2_2_0.Coordinate> dumpCoordinateSequence( CoordinateSequence cs) { List<de.micromata.opengis.kml.v_2_2_0.Coordinate> result = new ArrayList<de.micromata.opengis.kml.v_2_2_0.Coordinate>( cs.size()); for (int i = 0; i < cs.size(); i++) { double x = cs.getOrdinate(i, 0); double y = cs.getOrdinate(i, 1); double z = Double.NaN; if (cs.getDimension() >= 3 || hasHeightTemplate) { z = cs.getOrdinate(i, 2); } de.micromata.opengis.kml.v_2_2_0.Coordinate c; if (Double.isNaN(z)) { c = new de.micromata.opengis.kml.v_2_2_0.Coordinate(x, y); } else { c = new de.micromata.opengis.kml.v_2_2_0.Coordinate(x, y, z); } result.add(c); } return result; } public void applyExtrusion(de.micromata.opengis.kml.v_2_2_0.Geometry kmlGeometry) { if(kmlGeometry instanceof de.micromata.opengis.kml.v_2_2_0.Polygon) { de.micromata.opengis.kml.v_2_2_0.Polygon polygon = (de.micromata.opengis.kml.v_2_2_0.Polygon) kmlGeometry; polygon.setExtrude(extrudeEnabled); polygon.setAltitudeMode(AltitudeMode.RELATIVE_TO_GROUND); } else if(kmlGeometry instanceof de.micromata.opengis.kml.v_2_2_0.LinearRing) { de.micromata.opengis.kml.v_2_2_0.LinearRing ring = (de.micromata.opengis.kml.v_2_2_0.LinearRing) kmlGeometry; ring.setExtrude(extrudeEnabled); ring.setTessellate(true); ring.setAltitudeMode(AltitudeMode.RELATIVE_TO_GROUND); } else if(kmlGeometry instanceof de.micromata.opengis.kml.v_2_2_0.LineString) { de.micromata.opengis.kml.v_2_2_0.LineString ls = (de.micromata.opengis.kml.v_2_2_0.LineString) kmlGeometry; ls.setExtrude(extrudeEnabled); ls.setTessellate(true); ls.setAltitudeMode(AltitudeMode.RELATIVE_TO_GROUND); } else if(kmlGeometry instanceof de.micromata.opengis.kml.v_2_2_0.Point) { de.micromata.opengis.kml.v_2_2_0.Point point = (de.micromata.opengis.kml.v_2_2_0.Point) kmlGeometry; point.setExtrude(extrudeEnabled); point.setAltitudeMode(AltitudeMode.RELATIVE_TO_GROUND); } else if(kmlGeometry instanceof MultiGeometry) { de.micromata.opengis.kml.v_2_2_0.MultiGeometry mg = (de.micromata.opengis.kml.v_2_2_0.MultiGeometry) kmlGeometry; for(de.micromata.opengis.kml.v_2_2_0.Geometry g : mg.getGeometry()) { applyExtrusion(g); } } } } }