/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-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;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import org.xml.sax.SAXException;
/**
* LEVEL1 saxGML4j GML filter: Sends basic alerts for GML types to
* GMLFilterGeometry.
*
* <p>
* This filter separates and passes GML events to a GMLHandlerGeometry. The
* main simplification that it performs is to pass along coordinates as an
* abstracted method call, regardless of their notation in the GML (Coord vs.
* Coordinates). This call turns the coordinates into doubles and makes sure
* that it distinguishes between 2 and 3 value coordinates.
* </p>
*
* <p>
* The filter also handles some more subtle processing, including handling
* different delimiters (decimal, coordinate, tuple) that may be used by more
* outlandish GML generators.
* </p>
*
* <p></p>
*
* @author Rob Hranac, Vision for New York
*
* @source $URL$
* @version $Id$
*/
public class GMLFilterDocument extends org.xml.sax.helpers.XMLFilterImpl {
/** The logger for the GML module */
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.gml");
// Static Globals to handle some expected elements
/** GML namespace string */
private static final String GML_NAMESPACE = "http://www.opengis.net/gml";
/** Coord name */
private static final String COORD_NAME = "coord";
/** Coordinates name */
private static final String COORDINATES_NAME = "coordinates";
/** X Coordinate name */
private static final String X_NAME = "X";
/** Y Coordinate name */
private static final String Y_NAME = "Y";
/** Z Coordinate name */
private static final String Z_NAME = "Z";
/** Sub geometry elements that may be passed in GML */
private static final java.util.Collection SUB_GEOMETRY_TYPES = new java.util.Vector(java.util.Arrays
.asList(new String[] { "outerBoundaryIs", "innerBoundaryIs" }));
/** Base geometry elements that may be passed in GML */
private static final java.util.Collection BASE_GEOMETRY_TYPES = new java.util.Vector(java.util.Arrays
.asList(new String[] {
"Point", "LineString", "Polygon", "LinearRing", "Box",
"MultiPoint", "MultiLineString", "MultiPolygon",
"GeometryCollection"
}));
/** Added by Sean Geoghegan to store character data chunks */
private StringBuffer buffer = new StringBuffer();
/** Parent of the filter: must implement GMLHandlerGeometry. */
private GMLHandlerGeometry parent;
/** Handles all coordinate parsing. */
private CoordinateReader coordinateReader = new CoordinateReader();
/** Whether or not this parser should consider namespaces. */
private boolean namespaceAware = true;
/**
* Constructor with parent.
*
* @param parent Parent of the filter: must implement GMLHandlerGeometry.
*/
public GMLFilterDocument(GMLHandlerGeometry parent) {
super();
LOGGER.entering("GMLFilterDocument", "new", parent);
this.parent = parent;
LOGGER.exiting("GMLFilterDocument", "new");
}
/**
* Checks for GML element start and - if not a coordinates element - sends
* it directly on down the chain to the appropriate parent handler. If it
* is a coordinates (or coord) element, it uses internal methods to set
* the current state of the coordinates reader appropriately.
*
* <p>
* Modified by Sean Geoghegan to create new StringBuffers when entering a
* coord or coordinate element.
* </p>
*
* @param namespaceURI The namespace of the element.
* @param localName The local name of the element.
* @param qName The full name of the element, including namespace prefix.
* @param atts The element attributes.
*
* @throws SAXException Some parsing error occurred while reading
* coordinates.
*/
public void startElement(String namespaceURI, String localName,
String qName, org.xml.sax.Attributes atts) throws SAXException {
LOGGER.entering("GMLFilterDocument", "startElement",
new Object[] { namespaceURI, localName, qName, atts });
/* if at a GML element, do some checks to determine
* how to handle the element
*/
if (namespaceURI != null && namespaceURI.equals(GML_NAMESPACE)) {
// if geometry, pass it on down the filter chain
if (BASE_GEOMETRY_TYPES.contains(localName)) {
parent.geometryStart(localName, atts);
} else if (SUB_GEOMETRY_TYPES.contains(localName)) {
parent.geometrySub(localName);
} else if (COORDINATES_NAME.equals(localName)) {
// if coordinate, set one of the internal coordinate methods
coordinateReader.insideCoordinates(true, atts);
buffer = new StringBuffer();
} else if (COORD_NAME.equals(localName)) {
coordinateReader.insideCoord(true);
buffer = new StringBuffer();
} else if (X_NAME.equals(localName)) {
buffer = new StringBuffer();
coordinateReader.insideX(true);
} else if (Y_NAME.equals(localName)) {
buffer = new StringBuffer();
coordinateReader.insideY(true);
} else if (Z_NAME.equals(localName)) {
buffer = new StringBuffer();
coordinateReader.insideZ(true);
} else {
parent.startElement(namespaceURI, localName, qName, atts);
}
} else {
/* all non-GML elements passed on down the filter chain without
* modification
*/
parent.startElement(namespaceURI, localName, qName, atts);
}
LOGGER.exiting("GMLFilterDocument", "startElement");
}
/**
* Reads the only internal characters read by pure GML parsers, which are
* coordinates. These coordinates are sent to the coordinates reader
* class, which interprets them appropriately, depending on its current
* state.
*
* <p>
* Modified by Sean Geoghegan to append character data to buffer when
* inside a coordinate or coord element. SAX doesn't guarentee that all
* the character data of an element will be passed to the character method
* in one call, it may be split up into chunks.
* </p>
*
* @param ch Raw coordinate string from the GML document.
* @param start Beginning character position of raw coordinate string.
* @param length Length of the character string.
*
* @throws SAXException Some parsing error occurred while reading
* coordinates.
*/
public void characters(char[] ch, int start, int length)
throws SAXException {
LOGGER.entering("GMLFilterDocument", "characters",
new Object[] { ch, new Integer(start), new Integer(length) });
/* the methods here read in both coordinates and coords and
* take the grunt-work out of this task for geometry handlers
* see the documentation for CoordinatesReader to see what this entails
*/
String rawCoordinates = new String(ch, start, length);
/* determines how to read coordinates, depending on
* what element we are currently inside
*/
if (coordinateReader.insideCoordinates()) {
buffer.append(rawCoordinates);
//coordinateReader.readCoordinates(rawCoordinates);
} else if (coordinateReader.insideCoord()) {
buffer.append(rawCoordinates);
//coordinateReader.readCoord(rawCoordinates);
} else {
/* all non-coordinate data passed on down the
* filter chain without modification
*/
parent.characters(ch, start, length);
}
LOGGER.exiting("GMLFilterDocument", "characters");
}
/**
* Checks for GML element end and - if not a coordinates element - sends
* it directly on down the chain to the appropriate parent handler. If it
* is a coordinates (or coord) element, it uses internal methods to set
* the current state of the coordinates reader appropriately.
*
* <p>
* Modified by Sean Geoghegan. When we reach the end of a coord or
* coordinate element, then the buffer is passed to the handler for
* processing.
* </p>
*
* @param namespaceURI The namespace of the element.
* @param localName The local name of the element.
* @param qName The full name of the element, including namespace prefix.
*
* @throws SAXException Some parsing error occurred while reading
* coordinates.
*/
public void endElement(String namespaceURI, String localName, String qName)
throws SAXException {
LOGGER.entering("GMLFilterDocument", "endElement",
new Object[] { namespaceURI, localName, qName });
/* if leaving a GML element, handle and pass to appropriate
* internal or external method
*/
if (namespaceURI.equals(GML_NAMESPACE) || !namespaceAware) {
// if geometry, pass on down the chain to appropriate handlers
if (BASE_GEOMETRY_TYPES.contains(localName)) {
parent.geometryEnd(localName);
} else if (SUB_GEOMETRY_TYPES.contains(localName)) {
parent.geometrySub(localName);
} else if (COORDINATES_NAME.equals(localName)) {
// Convert the string buffer to a string and process the
// coordinate, then end the coordinate status in the handler.
coordinateReader.readCoordinates(buffer.toString());
coordinateReader.insideCoordinates(false);
} else if (COORD_NAME.equals(localName)) {
coordinateReader.readCoord(buffer.toString());
coordinateReader.insideCoord(false);
} else if (X_NAME.equals(localName)) {
coordinateReader.readCoord(buffer.toString());
coordinateReader.insideX(false);
} else if (Y_NAME.equals(localName)) {
coordinateReader.readCoord(buffer.toString());
coordinateReader.insideY(false);
} else if (Z_NAME.equals(localName)) {
coordinateReader.readCoord(buffer.toString());
coordinateReader.insideZ(false);
} else if (!namespaceAware) {
/* if not namespace aware, then just pass element through;
* otherwise, there is some error in the GML
*/
parent.endElement(namespaceURI, localName, qName);
} else {
parent.endElement(namespaceURI, localName, qName);
}
//else { throw new SAXException("Unrecognized GML element."); }
} else {
/* all non-GML elements passed on down the filter chain
* without modification
*/
parent.endElement(namespaceURI, localName, qName);
}
LOGGER.exiting("GMLFilterDocument", "endElement");
}
/**
* Simplifies the parsing process for GML coordinate elements.
*
* <p>
* One of the more annoying features of GML (from a SAX parsing
* perspective) is the dual coord and coordinate representation of
* coordinates. To further complicate the matter, delimiters for the
* coordinates element are quite flexible. This class hides all that
* nasty complexity beneath a benign exterior and greatly reduces the
* complexity of the GMLFilterDocument code.
* </p>
*/
private class CoordinateReader {
/** Flag for indicating not inside any tag. */
private static final int NOT_INSIDE = 0;
/** Flag for indicating inside coord tag. */
private static final int INSIDE_COORD = 1;
/** Flag for indicating inside coordinates tag. */
private static final int INSIDE_COORDINATES = 2;
/** Flag for indicating inside X tag. */
private static final int INSIDE_X = 1;
/** Flag for indicating inside Y tag. */
private static final int INSIDE_Y = 2;
/** Flag for indicating inside Z tag. */
private static final int INSIDE_Z = 3;
/** Remembers where we are inside the GML coordinate stream. */
private int insideOuterFlag = NOT_INSIDE;
/** Remembers where we are inside the GML coordinate stream. */
private int insideInnerFlag = NOT_INSIDE;
/** Remembers last X coordinate read. */
private Double x = new Double(Double.NaN);
/** Remembers last Y coordinate read. */
private Double y = new Double(Double.NaN);
/** Remembers last Z coordinate read. */
private Double z = new Double(Double.NaN);
/**
* Stores requested delimiter for coordinate separation; default = ','
*/
private String coordinateDelimeter = ",";
/** Stores requested delimiter for tuple separation; default = ' ' */
private String tupleDelimeter = " ";
/** Stores requested delimiter for decimal separation; default = '.' */
private StringBuffer decimalDelimeter = new StringBuffer(".");
/**
* Remembers whether or not the standard decimal is used, to speed up
* parsing.
*/
private boolean standardDecimalFlag = true;
/**
* Empty constructor.
*/
public CoordinateReader() {
}
/**
* Reads raw coordinates from the GML and returns them to the parent as
* neat functions.
*
* @param coordinateString Raw coordinate string from the GML document.
*
* @throws SAXException Some parsing error occurred while reading
* coordinates.
*/
public void readCoordinates(String coordinateString)
throws SAXException {
/* if non-standard delimiter, replace it with
* standard ',' through the entire string
*/
if (!standardDecimalFlag) {
coordinateString = coordinateString.replace(decimalDelimeter
.charAt(0), '.');
}
// separate tuples and loop through the set
StringTokenizer coordinateSets = new StringTokenizer(coordinateString
.trim(), tupleDelimeter);
StringTokenizer coordinates;
// loop through each of the coordinate sets.
// Depending on the number of coordinates found,
// call the correct parent coordinate class
while (coordinateSets.hasMoreElements()) {
coordinates = new StringTokenizer(coordinateSets.nextToken(),
coordinateDelimeter);
x = new Double(coordinates.nextToken().trim());
y = new Double(coordinates.nextToken().trim());
if (coordinates.hasMoreElements()) {
z = new Double(coordinates.nextToken().trim());
parent.gmlCoordinates(x.doubleValue(), y.doubleValue(),
z.doubleValue());
} else {
parent.gmlCoordinates(x.doubleValue(), y.doubleValue());
}
}
}
/**
* Reads a coord string. Note that this string is actually inside an
* X, Y, Z tag and is not directly returned by the parent function,
* unlike the readCoordinates method.
*
* @param coordString The raw coordinate string from the XML document.
*/
public void readCoord(String coordString) {
// if non-standard delimiter, replace it with standard ','
// through the entire string
if (!standardDecimalFlag) {
coordString = coordString.replace(decimalDelimeter.charAt(0),
'.');
}
// determine which coord string we are inside
// set internal x,y,z values depending on the return
switch (insideInnerFlag) {
case INSIDE_X:
x = new Double(coordString.trim());
break;
case INSIDE_Y:
y = new Double(coordString.trim());
break;
case INSIDE_Z:
z = new Double(coordString.trim());
break;
default:
break;
}
}
/**
* Sets an entrance into a coordinates element.
*
* @param isInside Sets whether or not we are inside a coordinates
* tag.
* @param atts Passes the coordinates tag attributes.
*/
public void insideCoordinates(boolean isInside,
org.xml.sax.Attributes atts) {
this.insideCoordinates(isInside);
}
/**
* Sets an entrance into a coordinates element.
*
* @param isInside Sets whether or not we are inside a coordinates tag.
*/
public void insideCoordinates(boolean isInside) {
if (isInside) {
insideOuterFlag = INSIDE_COORDINATES;
} else {
insideOuterFlag = NOT_INSIDE;
}
}
/**
* Sets an entrance into a coord element.
*
* @param isInside Sets whether or not we are inside a coord tag.
*
* @throws SAXException if error occurs in reading
*/
public void insideCoord(boolean isInside) throws SAXException {
// if entering coord tag, simply set our internal flag for this
if (isInside) {
insideOuterFlag = INSIDE_COORD;
} else {
// if leaving coord tag, send coordinates to parent and
// set all internal values to null equivalent.
// if coordinates exist, send on down the filter chain
// otherwise, throw an exception
if ((!x.isNaN()) && (!y.isNaN()) && (z.isNaN())) {
parent.gmlCoordinates(x.doubleValue(), y.doubleValue());
} else if ((!x.isNaN()) && (!y.isNaN()) && (!z.isNaN())) {
parent.gmlCoordinates(x.doubleValue(), y.doubleValue(),
z.doubleValue());
}
//else {
x = new Double(Double.NaN);
y = new Double(Double.NaN);
z = new Double(Double.NaN);
insideOuterFlag = NOT_INSIDE;
}
}
/**
* Sets an entrance into an X element.
*
* @param isInside Sets whether or not we are inside an X tag.
*/
public void insideX(boolean isInside) {
if (isInside) {
insideInnerFlag = INSIDE_X;
} else {
insideInnerFlag = NOT_INSIDE;
}
}
/**
* Sets an entrance into a Y element.
*
* @param isInside Sets whether or not we are inside a Y tag.
*/
public void insideY(boolean isInside) {
if (isInside) {
insideInnerFlag = INSIDE_Y;
} else {
insideInnerFlag = NOT_INSIDE;
}
}
/**
* Sets an entrance into a Z element.
*
* @param isInside Sets whether or not we are inside a Z tag.
*/
public void insideZ(boolean isInside) {
if (isInside) {
insideInnerFlag = INSIDE_Z;
} else {
insideInnerFlag = NOT_INSIDE;
}
}
/**
* Queries whether or not we are inside a coordinates element.
*
* @return true/false
*/
public boolean insideCoordinates() {
return insideOuterFlag == INSIDE_COORDINATES;
}
/**
* Queries whether or not we are inside a coord element.
*
* @return true/false
*/
public boolean insideCoord() {
return insideOuterFlag == INSIDE_COORD;
}
}
}