/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-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.styling; import java.net.URI; import java.util.ArrayList; import java.util.Hashtable; import org.geotools.data.memory.MemoryDataStore; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.filter.ExpressionDOMParser; import org.geotools.referencing.CRS; import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.vividsolutions.jts.geom.Geometry; public class SLDInlineFeatureParser { /** hash table that takes a epsg# to its definition**/ private static Hashtable<Integer,CoordinateReferenceSystem> SRSLookup = new Hashtable<Integer,CoordinateReferenceSystem>(); public SimpleFeatureType featureType=null; public MemoryDataStore dataStore = null; Node rootNode = null; ArrayList<Feature> features= new ArrayList<Feature>(); CoordinateReferenceSystem SRS = null; // default EPSG#. private static int uniqueNumber = 0; public SLDInlineFeatureParser(Node root) throws Exception { //handle SimpleFeatureCollection or Feature Tag boolean isFeatureCollection = false; if (!(isSimple(root))) //make sure this isnt too complex to parse easily. { throw new Exception("couldnt parse the SLD Inline features!");//shouldnt get here } Node fc = getNode(root,"FeatureCollection"); if (fc != null) { isFeatureCollection = true; root = fc;//decend down one level } featureType = makeFeatureType(root,isFeatureCollection); if (featureType == null) throw new Exception("SLD InlineFeature Parser - couldnt determine a FeatureType. See help for whats supported.");//shouldnt get here makeFeatures(root,isFeatureCollection); if (features.size() ==0) throw new Exception("SLD InlineFeature Parser - didnt find any features!"); buildStore(); } /** * 1. we have a FeatureType (cf. makeFeatureType) * 2. we iterate through either the SimpleFeatureCollection or the _Feature set * 3. we build a Feature for each element of the set * 4. we stick it in the features list * * For example: * * <InlineFeature> * <Dave> * ... * </Dave> * <Dave> * ... * </Dave> * </InlineFeature> * * --- OR ---- * * <InlineFeature> * <FeatureCollection> * <featureMember> * <Dave> * ... * </Dave> * </featureMember> * <featureMember> * <Dave> * ... * </Dave> * </featureMember> * </FeatureCollection> * </InlineFeature> * * * @param root will point at either "<InlineFeature>" or "<FeatureCollection> */ private void makeFeatures(Node root,boolean isCollection) throws Exception { //iterate through each of the elements inside the root NodeList children = root.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } String childName = child.getLocalName(); if (childName == null) childName = child.getNodeName(); if (childName.equalsIgnoreCase("boundedBy")) //be nice and ignore this continue; if (isCollection) { //decend into the featureMember child = descend(child); } if (child ==null) throw new Exception("SLD inlineFeature Parser - couldnt extract a feature from the dom."); SimpleFeature f = parseFeature(child,featureType); features.add(f); } } /** * simple - child points to a <featureMember>, we want it to point to the element inside! * * in general, this will find the 1st element node inside the node. * * @param child */ private Node descend(Node root) { NodeList children = root.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } return child; } return null; //nothing inside } /** * Parse the feature pointed to by this node. * * See the makeFeatureType() function - this is very similiar except it does a little parsing. * * * @param feature - points to the actual feature ie. "<Person>" * @param featureType */ private SimpleFeature parseFeature(Node feature, SimpleFeatureType featureType) throws Exception { Object[] nullAtts = new Object[featureType.getAttributeCount()]; // initialized to nulls SimpleFeature f = SimpleFeatureBuilder.build(featureType, nullAtts, null); NodeList children = feature.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } String childName = child.getLocalName(); if (childName == null) { childName = child.getNodeName(); } Object value = getValue(child); try{ f.setAttribute(childName,value); } catch (Exception e) { e.printStackTrace(); // we hid this from the user } } return f; } /** * Given a node, determine if its a geometry or a string attribute * return the corresponding value. * @param child */ private Object getValue(Node root) throws Exception { NodeList children = root.getChildNodes(); StringBuffer strVal = new StringBuffer(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) ) { continue; } if (child.getNodeType() == Node.TEXT_NODE) { strVal.append(child.getNodeValue()); //might get here multiple times -- see sax spec } //we have a nested element! Assume its a geometry if (child.getNodeType() == Node.ELEMENT_NODE) { return parseGeometry(child); } } return strVal; } /** * <Person> * <location> * <gml:Point>... * </gml:Point> * </location> * </Person> * * Decend a level and then pass it off to the geometry parser. * * NOTE: also handles SRS information: * * <gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"> * * TODO: handle more than just epsg for CRS * * @param root -- points to "<gml:Point>" */ private Geometry parseGeometry(Node root) throws Exception { NamedNodeMap atts = root.getAttributes(); if (SRS == null) //try to avoid parsing more than once. { Node srsName = atts.getNamedItem("srsName"); if ( srsName != null) { parseSRS(srsName.getNodeValue()); } } ExpressionDOMParser parser = new ExpressionDOMParser( CommonFactoryFinder.getFilterFactory2(null)); return parser.gml( root ); } /** * * Checks to make sure we're not going to shoot ourselves in the foot. * if InlineFeature has a FeatureCollection, then thats the only node * ie. no set of FeatureCollections or SimpleFeatureCollection mixed with a set of Feature * * Other stuff as we think of them. * * @param root SLD root node -- "InlineFeature" * @return true if okay, otherwise exception */ private boolean isSimple(Node root) throws Exception { // if there's a <FeatureCollection>, thats the only child // if there was a <FeatureCollection>, then descend into it // check to make sure there are only <featureMember> in it int foundFeature = 0; int foundFC = 0; Node fcNode = null; String featureName = null; NodeList children = root.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } String childName = child.getLocalName(); if (childName == null) childName = child.getNodeName(); if (childName.equalsIgnoreCase("FeatureCollection")) { (foundFC)++; fcNode = child; } else { if (featureName == null) featureName = childName; if (!(childName.equalsIgnoreCase(featureName))) throw new Exception ("SLD inline feature parser - it appear that there is >1 feature type present. I got a "+ childName+ " when I was expecting a "+featureName+" tag"); } } if (foundFC >1) throw new Exception ("SLD - UserLayer, inline feature parser - found >1 FeatureCollection. Not supported"); if ( (foundFC >0) && (foundFeature>0) ) throw new Exception ("SLD - UserLayer, inline feature parser - found SimpleFeatureCollection and featureMembers. Not supported"); if (foundFC ==0) return true; featureName = null; //otherwise decend into the SimpleFeatureCollection and check to make sure it only contains features children = fcNode.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } String childName = child.getLocalName(); if (childName == null) childName = child.getNodeName(); if (childName.equalsIgnoreCase("featureMember")) foundFeature++; else if (childName.equalsIgnoreCase("boundedBy")) { //this is okay -- we'll be nice and ignore it. } else if (childName.equalsIgnoreCase("FeatureCollection")) { throw new Exception ("SLD - UserLayer, inline feature parser - found a node of type FeatureCollection. Expected a featureMember - dont support nested collections."); } else throw new Exception ("SLD - UserLayer, inline feature parser - found a node of type '"+child.getLocalName()+"' and dont understand it. Expected a featureMember."); } return true; } /** * */ private void buildStore() { dataStore = new MemoryDataStore( (SimpleFeature[]) features.toArray(new SimpleFeature[features.size()])); } /** * 1. get an actual Feature Node * 2. go through each of its subtags * 3. if that subtag contains a geometry, then it an attribute of type geometry, otherwise string * * NOTE: we set the namespace to be "http://temp.inline.feature.sld.com" * NOTE: we set the featuretype name to be whatever the enclosing tag is. For example: * <FeatureCollection> * <gml:featureMember> * <tiger:point_landmark fid="point_landmark.490053"> * <tiger:wkb_geometry> * <gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"> * <gml:coordinates decimal="." cs="," ts=" ">-73.983597,40.736308</gml:coordinates> * </gml:Point> * </tiger:wkb_geometry> * <tiger:laname>Cabrini Medical Center</tiger:laname> * </tiger:point_landmark> * <gml:featureMember> * </FeatureCollection> * * Would have a Featuretype name of "tiger:wkb_geometry" with 2 attributes: * wkb_geometry -- geometry * laname -- string * * @param root */ private SimpleFeatureType makeFeatureType(Node root,boolean isCollection) throws Exception { Node feature = null; //get a Feature node Node featureMember =root; if (isCollection) featureMember = getNode(root,"featureMember"); //next node under featureMember what we want. Unless its a boundedBy, in which case we dont want it. NodeList children = featureMember.getChildNodes(); // look for next element that's not "boundedBy" for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } String childName = child.getLocalName(); if (childName == null) { childName = child.getNodeName(); } if (!(childName.equalsIgnoreCase("boundedBy")) ) { feature = child; break; } } if (feature == null) throw new Exception ("couldnt find a Feature in the Inline Features!"); //okay, we have a feature, we now need to figure out its feature type. // method: // step through each node, its name is a new element in the Feature // we look for any internal tags (nesting). There there are, we check to see if its a geometry // otherwise the type is string. // simple! String featureName = feature.getLocalName(); if (featureName == null) { featureName = feature.getNodeName(); } //DJB:I considered making each featuretype unique (thats the uniquenumber), but decided against // it so that the standard feature type filtering stuff would work ("<FeatureTypeStyle>" // and <FeatureTypeConsraint> SimpleFeatureTypeBuilder build = new SimpleFeatureTypeBuilder(); build.setName(featureName); build.setNamespaceURI(new URI("http://temp.inline.feature.sld.com")); children = feature.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } String childName = child.getLocalName(); if (childName == null) { childName = child.getNodeName(); } //AttributeDescriptor attType = null; //okay, have a tag, check to see if its a geometry if (isGeometry(child)) { // force full geometry parsing so that we get to know the declared SRS getValue(child); build.add(childName, Geometry.class, SRS); } else { build.add(childName,String.class); } } return build.buildFeatureType(); } /** * looks for a nested attribute - assumes that this is a geometry. * TODO: be much smarter * @param child */ private boolean isGeometry(Node root) { NodeList children = root.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } //we have a nested element! return true; } return false; } /** * Give a node and the name of a child of that node, find its (string) value. * This doesnt do anything complex. * * @param parentNode * @param wantedChildName */ public Node getNode(Node parentNode, String wantedChildName) { NodeList children = parentNode.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if ((child == null) || (child.getNodeType() != Node.ELEMENT_NODE)) { continue; } String childName = child.getLocalName(); if (childName == null) { childName = child.getNodeName(); } if (childName.equalsIgnoreCase(wantedChildName)) { return child; } } return null; } public synchronized int getUID() { return uniqueNumber++; } /** * expected input: * "http://www.opengis.net/gml/srs/epsg.xml#4326" * NOTE: only supports epsg#s. * @param srs */ private void parseSRS(String srs) throws Exception { if (srs ==null) return; String epsgCode = srs.substring(srs.indexOf('#')+1); int srsnum = Integer.parseInt(epsgCode); SRS= getSRS(srsnum); } /** * simple way of getting epsg #. * We cache them so that we dont have to keep reading the DB or the epsg.properties file. * I cannot image a system with more than a dozen CRSs in it... * * @param epsg */ private CoordinateReferenceSystem getSRS(int epsg) throws Exception { CoordinateReferenceSystem result = (CoordinateReferenceSystem) SRSLookup.get( new Integer(epsg) ); if (result == null) { //make and add to hash result = CRS.decode("EPSG:"+epsg); SRSLookup.put( new Integer(epsg) , result); } return result; } }