/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2003-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.gml.producer; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureReader; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureCollectionIteration; import org.geotools.feature.type.DateUtil; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.gml.producer.GeometryTransformer.GeometryTranslator; import org.geotools.referencing.CRS; import org.geotools.xml.transform.TransformerBase; import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.FeatureType; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.Name; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.geometry.BoundingBox; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import org.xml.sax.helpers.NamespaceSupport; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; /** * FeatureTransformer provides a mechanism for converting Feature objects into * (hopefully) valid gml. This is a work in progress, so please be patient. A * simple example of how to use this class follows: *
* SimpleFeatureCollection collection; // can also use FeatureReader!! * OutputStream out; * FeatureTransformer ft = new FeatureTransformer(); * // set the indentation to 4 spaces * ft.setIndentation(4); * // this will allow Features with the FeatureType which has the namespace * // "http://somewhere.org" to be prefixed with xxx... * ft.getFeatureNamespaces().declarePrefix("xxx","http://somewhere.org"); * // transform * ft.transform(collection,out); ** The above example assumes a homogenous collection of Features whose * FeatureType has the namespace "http://somewhere.org" but note that not * all DataSources currently provide FeatureTypes with a namespace... There * are two other mechanisms for prefixing your Features.
* FeatureType fc; * FeatureTransformer ft = new FeatureTransformer(); * ft.getFeatureTypeNamespaces().declareNamespace(fc,"xxx","http://somewhere.org"); ** 2) Provide a default namespace for any Features whose FeatureType either has * an empty namespace, OR, has not been mapped using the previous method. This * is basically a catch-all mechanism. *
* FeatureTransformer ft = new FeatureTransformer(); * ft.getFeatureTypeNamespaces().declareDefaultNamespace("xxx","http://somewhere.org"); **
* Note that the setGmlPrefixing() interacts with this * occasionally, since it will hack in a gml prefix to a boundedBy * attribute included in the featureType. If gml prefixing is on, and * featureBounding is on, then the bounds from the attribute will be used. * If gml prefixing is off, then that boundedBy attribute will * presumably be in its own namespace, and so the automatic gml boundedBy * will not conflict, so both will be printed, with the automatic one * deriving its bounds from the boundedBy attribute and any other * geometries in the feature *
* * @param featureBounding true if the bounds of the feature should * be automatically calculated and included as a gml:boundedBy in * the gml output. Note this puts a good bit of bandwidth overhead * on the output. Default is false */ public void setFeatureBounding(boolean featureBounding) { this.featureBounding = featureBounding; } /** * If true, enables the generation of the full collection bounds. Depending on the * collection being generated in output, this operation can be extremely expensive. ** Defaults to true (for backwards compatibility), disable explicitly if you * don't want feature collection bounds to be generated. * @param collectionBounding */ public void setCollectionBounding(boolean collectionBounding) { this.collectionBounding = collectionBounding; } public org.geotools.xml.transform.Translator createTranslator( ContentHandler handler) { FeatureTranslator t = createTranslator(handler, collectionPrefix, collectionNamespace, featureTypeNamespaces, schemaLocation); java.util.Enumeration prefixes = nsLookup.getPrefixes(); //setGmlPrefixing(true); t.setNumDecimals(numDecimals); t.setGmlPrefixing(prefixGml); t.setSrsName(srsName); t.setLockId(lockId); t.setFeatureBounding(featureBounding); t.setCollectionBounding(collectionBounding); while (prefixes.hasMoreElements()) { String prefix = prefixes.nextElement().toString(); String uri = nsLookup.getURI(prefix); t.getNamespaceSupport().declarePrefix(prefix, uri); } return t; } /** * Template method for creating the translator. */ protected FeatureTranslator createTranslator( ContentHandler handler, String prefix, String ns, FeatureTypeNamespaces featureTypeNamespaces, SchemaLocationSupport schemaLocationSupport ) { return new FeatureTranslator( handler, prefix, ns, featureTypeNamespaces, schemaLocationSupport ); } public static class FeatureTypeNamespaces { Map lookup = new HashMap(); NamespaceSupport nsSupport; String defaultPrefix = null; public FeatureTypeNamespaces(NamespaceSupport nsSupport) { this.nsSupport = nsSupport; } public void declareDefaultNamespace(String prefix, String nsURI) { defaultPrefix = prefix; nsSupport.declarePrefix(prefix, nsURI); } public void declareNamespace(FeatureType type, String prefix, String nsURI) { lookup.put(type, prefix); nsSupport.declarePrefix(prefix, nsURI); } public String findPrefix(FeatureType type) { String pre = (String) lookup.get(type); if (pre == null) { pre = defaultPrefix; } return pre; } public String toString() { return "FeatureTypeNamespaces[Default: " + defaultPrefix + ", lookUp: " + lookup.keySet() +"]"; } } /** * Outputs gml without any fancy indents or newlines. */ public static class FeatureTranslator extends TranslatorSupport implements FeatureCollectionIteration.Handler { String fc = "FeatureCollection"; protected GeometryTransformer.GeometryTranslator geometryTranslator; String memberString; String currentPrefix; FeatureTypeNamespaces types; boolean prefixGml = false; boolean featureBounding = false; boolean collectionBounding = true; /** * The string representing the Spatial Reference System of the data. *
* This value should be determined by looking at the first * GeometryAttributeType encountered (but this way GeoServer can override * everything and be divorced from the actual (lack of) abilities of the underlying * DataStore). */ String srsName = null; /** * Will be 0 - if unknown; 2 if normal and 3 if working with 3D coordinates. *
* This value will be set based on looking at the *first* GeometryAttributeType encountered, * a similar approach should be taken for determining the SRID name. * * @since 2.4.1 */ int dimension = 0; String lockId = null; ContentHandler handler; private boolean running = true; /** * Constructor with handler. * * @param handler the handler to use. * @param prefix prefix * @param ns namespace * @param types Capture namespace and prefix information for types * @param schemaLoc Schema location information */ public FeatureTranslator(ContentHandler handler, String prefix, String ns, FeatureTypeNamespaces types, SchemaLocationSupport schemaLoc) { super(handler, prefix, ns, schemaLoc); geometryTranslator = createGeometryTranslator( handler ); this.types = types; this.handler = handler; getNamespaceSupport().declarePrefix(geometryTranslator .getDefaultPrefix(), geometryTranslator.getDefaultNamespace()); memberString = geometryTranslator.getDefaultPrefix() + ":featureMember"; } /** * Method to be subclassed to return a custom geometry translator, mostly for gml3 * geometry support. * @param handler * @return */ protected GeometryTranslator createGeometryTranslator( ContentHandler handler ) { return new GeometryTransformer.GeometryTranslator( handler ); } protected GeometryTranslator createGeometryTranslator( ContentHandler handler, int numDecimals ) { return new GeometryTransformer.GeometryTranslator( handler, numDecimals ); } /** * @param handler * @param numDecimals * @param useDummyZ * @return */ protected GeometryTranslator createGeometryTranslator( ContentHandler handler, int numDecimals, boolean useDummyZ ) { return new GeometryTransformer.GeometryTranslator( handler, numDecimals, useDummyZ ); } /** * Set up a GeometryTranslator for working with content of the indicate * dimension. *
* This method can be used by code explicitly wishing to output 2D ordinates.
*
* @since 2.4.1
* @param handler
* @param numDecimals
* @param dimension
* @return GeometryTranslator that will delegate a CoordinateWriter configured with the above parameters
*/
protected GeometryTranslator createGeometryTranslator( ContentHandler handler, int numDecimals, int dimension ) {
return new GeometryTranslator( handler, "gml",GMLUtils.GML_URL, numDecimals, false, dimension);
}
void setGmlPrefixing(boolean prefixGml) {
this.prefixGml = prefixGml;
}
void setFeatureBounding(boolean bounding) {
this.featureBounding = bounding;
}
void setCollectionBounding(boolean collectionBounding) {
this.collectionBounding = collectionBounding;
}
void setSrsName(String srsName) {
this.srsName = srsName;
}
void setNumDecimals(int numDecimals) {
geometryTranslator = createGeometryTranslator( handler, numDecimals );
}
void setUseDummyZ(boolean useDummyZ) {
geometryTranslator = createGeometryTranslator(handler,
geometryTranslator.getNumDecimals(), useDummyZ);
}
/** If set to 3 the real z value from the coordinates will be used */
void setDimension( int dimension ){
geometryTranslator = createGeometryTranslator(handler, geometryTranslator.getNumDecimals(), dimension );
}
public void setLockId(String lockId) {
this.lockId = lockId;
}
public FeatureTypeNamespaces getFeatureTypeNamespaces() {
return types;
}
public void encode(Object o) throws IllegalArgumentException {
try {
if (o instanceof FeatureCollection) {
SimpleFeatureCollection fc = (SimpleFeatureCollection) o;
FeatureCollectionIteration.iteration(this, fc);
} else if (o instanceof FeatureCollection[]) {
//Did FeatureResult[] so that we are sure they're all the same type.
//Could also consider collections here...
FeatureCollection[] results = (FeatureCollection[]) o;
startFeatureCollection();
if(collectionBounding) {
ReferencedEnvelope bounds = null;
for (int i = 0; i < results.length; i++) {
ReferencedEnvelope more = results[i].getBounds();
if( bounds == null ){
bounds = new ReferencedEnvelope( more );
}
else {
bounds.expandToInclude(more);
}
}
writeBounds(bounds);
} else {
writeNullBounds();
}
for (int i = 0; i < results.length; i++) {
handleFeatureIterator(DataUtilities.simple(results[i]).features());
}
endFeatureCollection();
} else if (o instanceof FeatureReader) {
// THIS IS A HACK FOR QUICK USE
FeatureReader
* Please take care when considering the prefix/namespace for the feature. It is defined
* by either:
* gml:boundedBy
element to output based on
* fc.getBounds()
*
* @param bounds
*
* @throws RuntimeException if it is thorwn while writing the element
* or coordinates
*/
public void writeBounds(BoundingBox bounds) {
try {
String boundedBy = geometryTranslator.getDefaultPrefix() + ":"
+ "boundedBy";
contentHandler.startElement("", "", boundedBy, NULL_ATTS);
Envelope env = null;
if (bounds != null){
env = new Envelope(new Coordinate(bounds.getMinX(), bounds.getMinY()),new Coordinate(bounds.getMaxX(), bounds.getMaxY()));
}
geometryTranslator.encode(env, srsName);
contentHandler.endElement("", "", boundedBy);
} catch (SAXException se) {
throw new RuntimeException(se);
}
}
/**
* writes null bounds to the output
*
* @throws RuntimeException if it is thorwn while writing the element
* or coordinates
*/
public void writeNullBounds() {
try {
String boundedBy = geometryTranslator.getDefaultPrefix() + ":boundedBy";
String nullBox = geometryTranslator.getDefaultPrefix() + ":null";
contentHandler.startElement("", "", boundedBy, NULL_ATTS);
contentHandler.startElement("", "", nullBox, NULL_ATTS);
contentHandler.characters("unknown".toCharArray(), 0, "unknown".length());
contentHandler.endElement("", "", nullBox);
contentHandler.endElement("", "", boundedBy);
} catch (SAXException se) {
throw new RuntimeException(se);
}
}
/**
* Sends sax for the ending of a feature collection.
*
* @param collection Feature collection we have just finished encoding
*/
public void endFeatureCollection(FeatureCollection,?> collection) {
endFeatureCollection();
}
/**
* Sends sax for the ending of a feature.
*
* @param f Feature (implementation assume a SimpleFeature)
*
* @throws RuntimeException if something goes wrong during encode it is wrapped up as a generic runtime exception
*/
public void endFeature(Feature f) {
try {
Name typeName = f.getType().getName();
String name = typeName.getLocalPart();
if (currentPrefix != null) {
name = currentPrefix + ":" + name;
}
contentHandler.endElement("", "", name);
contentHandler.endElement("", "", memberString);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* handles sax for an attribute.
*
* @param descriptor Property descriptor
* @param value Value being encoded for this property
*
* @throws RuntimeException Any problems are bundled up in a generic runtime exception
*/
public void handleAttribute(PropertyDescriptor descriptor, Object value) {
try {
if (value != null) {
String name = descriptor.getName().getLocalPart();
//HACK: this should be user configurable, along with the
//other gml substitutions I shall add.
if (prefixGml //adding this in since the extra boundedBy
//hacking should only need to be done for the weird
//cite tests, and having this check before the string
//equals should get us better performance. Albeit
//very slightly, but this method gets called millions
&& (name.equals("boundedBy")
&& value instanceof Geometry)) {
Envelope envelopeInternal = ((Geometry) value).getEnvelopeInternal();
CoordinateReferenceSystem crs = null;
if( descriptor instanceof GeometryDescriptor ){
GeometryDescriptor geometryDescriptor = (GeometryDescriptor) descriptor;
crs = geometryDescriptor.getCoordinateReferenceSystem();
}
ReferencedEnvelope
bounds = new ReferencedEnvelope( envelopeInternal, crs );
writeBounds( bounds);
} else {
String thisPrefix = currentPrefix;
if (prefixGml && gmlAtts.contains(name)) {
thisPrefix = "gml";
}
if (thisPrefix != null) {
name = thisPrefix + ":" + name;
}
contentHandler.startElement("", "", name, NULL_ATTS);
if (value instanceof Geometry) {
if( dimension == 0 ){
// lets look at the CRS
GeometryDescriptor geometryType = (GeometryDescriptor) descriptor;
CoordinateReferenceSystem crs = geometryType.getCoordinateReferenceSystem();
if( crs == null ){
// I won't even bother people with a warning
// (until DataStore quality has improved
dimension = 2; // the most sensible default
}
else {
dimension = crs.getCoordinateSystem().getDimension();
// note we could check the srsName here!
if( dimension == 3 ){
setDimension( dimension );
}
}
}
geometryTranslator.encode((Geometry) value, srsName);
} else if(value instanceof Date) {
String text = null;
if(value instanceof java.sql.Date)
text = DateUtil.serializeSqlDate((java.sql.Date) value);
else if(value instanceof java.sql.Time)
text = DateUtil.serializeSqlTime((java.sql.Time) value);
else
text = DateUtil.serializeDateTime((Date) value);
contentHandler.characters(text.toCharArray(), 0,
text.length());
} else {
String text = value.toString();
contentHandler.characters(text.toCharArray(), 0,
text.length());
}
contentHandler.endElement("", "", name);
}
}
//REVISIT: xsi:nillable is the proper xml way to handle nulls,
//but OGC people are fine with just leaving it out.
} catch (Exception e) {
throw new IllegalStateException("Could not transform "+descriptor.getName()+":"+e, e );
}
}
/**
* Handles sax for a feature.
*
*
*
* @param f Feature being encoded
*
* @throws RuntimeException Used to report any troubles during encoding
*/
public void handleFeature(Feature f) {
try {
contentHandler.startElement("", "", memberString, NULL_ATTS);
FeatureType type = f.getType();
String name = type.getName().getLocalPart();
String namespaceURI = type.getName().getNamespaceURI();
if( namespaceURI != null ){
currentPrefix = getNamespaceSupport().getPrefix( namespaceURI );
if( currentPrefix == null ){
currentPrefix = (String) type.getUserData().get("prefix");
if( currentPrefix != null ){
getNamespaceSupport().declarePrefix(currentPrefix, namespaceURI );
}
}
}
if (currentPrefix == null) {
currentPrefix = types.findPrefix(type);
}
if (currentPrefix == null ){
throw new IllegalStateException("FeatureType namespace/prefix unknown for " + name + "look up in: " + types);
}
else if ( currentPrefix.length() == 0 ) {
// must be the default prefix
}
else {
name = currentPrefix + ":" + name;
}
Attributes fidAtts = encodeFeatureId( f );
contentHandler.startElement("", "", name, fidAtts);
// encode the bounds if requested and the bounds are not missing or empty
if (featureBounding && f.getBounds() != null && !f.getBounds().isEmpty()) {
//HACK pt.2 see line 511, if the cite stuff wanted to hack
//in a boundedBy geometry, we don't want to do it twice.
//So if
if (prefixGml && (f.getProperty("boundedBy") != null)) {
//do nothing, since our hack will handle it.
} else {
writeBounds(f.getBounds());
}
}
} catch (Exception e) {
throw new IllegalStateException("Could not transform "+f.getIdentifier()+" :"+e, e );
}
}
protected Attributes encodeFeatureId( Feature f ) {
AttributesImpl fidAtts = new org.xml.sax.helpers.AttributesImpl();
String fid = f.getIdentifier().getID();
if (fid != null) {
fidAtts.addAttribute("", "fid", "fid", "fids", fid);
}
return fidAtts;
}
}
}