/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2015 - 2016, 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.gml2.simple; import java.text.FieldPosition; import java.text.NumberFormat; import java.util.Locale; import org.geotools.geometry.jts.coordinatesequence.CoordinateSequences; import org.geotools.gml2.GML; import org.geotools.xml.XMLUtils; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import org.xml.sax.helpers.NamespaceSupport; import com.vividsolutions.jts.geom.CoordinateSequence; /** * Helper class writing out GML elements and coordinates. Geared towards efficiency, write out * elements and ordinate lists with the minimim amount of garbage generation * * @author Andrea Aime - GeoSolutions * */ public class GMLWriter { static final QualifiedName COORDINATES = new QualifiedName(GML.NAMESPACE, "coordinates", "gml"); static final QualifiedName POS_LIST = new QualifiedName(GML.NAMESPACE, "posList", "gml"); /** * The min value at which the decimal notation is used (below it, the computerized scientific * one is used instead) */ private static final double DECIMAL_MIN = Math.pow(10, -3); /** * The max value at which the decimal notation is used (above it, the computerized scientific * one is used instead) */ private static final double DECIMAL_MAX = Math.pow(10, 7); /** * Used in coordinate formatting */ private static final FieldPosition ZERO = new FieldPosition(0); /** * The actual XML encoder */ ContentHandler handler; /** * All the namespaces known to the Encoder */ NamespaceSupport namespaces; /** * We use a StringBuffer because the date formatters cannot take a StringBuilder */ StringBuffer sb = new StringBuffer(); /** * The StringBuffer above gets dumped into this char buffer in order to pass the chars to the * handler */ char[] buffer; /** * Coordinates qualified name, with the right prefix */ private QualifiedName coordinates; /** * posList qualified name, with the right prefix */ private QualifiedName posList; /** * Scale used in truncate to reduce the number of decimals */ private double scale; /** To be used for formatting numbers, uses US locale. */ private final NumberFormat coordFormatter = NumberFormat.getInstance(Locale.US); /** * Whether we have to format in plain decimal numbers, or we can use scientific notation */ private boolean forceDecimal; /** * Create a new content handler * * @param delegate The actual XML writer * @param namespaces The namespaces known to the Encoder * @param numDecimals How many decimals to preserve when writing ordinates * @param forceDecimal If xs:decimal compliant encoding should be used, or not * @param gmlPrefix The GML namespace prefix */ public GMLWriter(ContentHandler delegate, NamespaceSupport namespaces, int numDecimals, boolean forceDecimal, String gmlPrefix) { this.handler = delegate; this.namespaces = namespaces; String gmlUri = namespaces.getURI(gmlPrefix); if (gmlUri == null) { gmlUri = GML.NAMESPACE; } this.coordinates = COORDINATES.derive(gmlPrefix, gmlUri); this.posList = POS_LIST.derive(gmlPrefix, gmlUri); this.coordFormatter.setMaximumFractionDigits(numDecimals); this.coordFormatter.setGroupingUsed(false); this.scale = Math.pow(10, numDecimals); this.forceDecimal = forceDecimal; } /** * @param locator * @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator) */ public void setDocumentLocator(Locator locator) { handler.setDocumentLocator(locator); } /** * @throws SAXException * @see org.xml.sax.ContentHandler#startDocument() */ public void startDocument() throws SAXException { handler.startDocument(); } /** * @throws SAXException * @see org.xml.sax.ContentHandler#endDocument() */ public void endDocument() throws SAXException { handler.endDocument(); } /** * @param prefix * @param uri * @throws SAXException * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String) */ public void startPrefixMapping(String prefix, String uri) throws SAXException { handler.startPrefixMapping(prefix, uri); } /** * @param prefix * @throws SAXException * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String) */ public void endPrefixMapping(String prefix) throws SAXException { handler.endPrefixMapping(prefix); } /** * @param uri * @param localName * @param qName * @param atts * @throws SAXException * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, * java.lang.String, org.xml.sax.Attributes) */ public void startElement(QualifiedName qn, Attributes atts) throws SAXException { String qualifiedName = qn.getQualifiedName(); if (qualifiedName == null) { qualifiedName = qualify(qn.getNamespaceURI(), qn.getLocalPart(), null); } if (atts == null) { atts =new AttributesImpl(); } if (qualifiedName != null) { String localName = null; if (qualifiedName.contains(":")) { localName = qualifiedName.split(":")[1]; } handler.startElement(qn.getNamespaceURI(), localName, qualifiedName, atts); } else { handler.startElement(qn.getNamespaceURI(), qn.getLocalPart(), null, atts); } } private String qualify(String uri, String localName, String qName) { if (qName == null) { String prefix = namespaces.getPrefix(uri); if (prefix == null) { return localName; } else { return prefix + ":" + localName; } } return qName; } /** * @param uri * @param localName * @param qName * @throws SAXException * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, * java.lang.String) */ public void endElement(QualifiedName qn) throws SAXException { String qualifiedName = qn.getQualifiedName(); if (qualifiedName == null) { qualifiedName = qualify(qn.getNamespaceURI(), qn.getLocalPart(), null); } if (qualifiedName != null) { handler.endElement(null, null, qualifiedName); } else { handler.endElement(qn.getNamespaceURI(), qn.getLocalPart(), null); } } /** * @param ch * @param start * @param length * @throws SAXException * @see org.xml.sax.ContentHandler#characters(char[], int, int) */ private void characters(char[] ch, int start, int length) throws SAXException { handler.characters(ch, start, length); } void characters(StringBuffer sb) throws SAXException { int length = sb.length(); if (buffer == null || buffer.length < length) { buffer = new char[length]; } sb.getChars(0, length, buffer, 0); characters(buffer, 0, length); } void characters(String s) throws SAXException { s = XMLUtils.removeXMLInvalidChars(s); int length = s.length(); if (buffer == null || buffer.length < length) { buffer = new char[length]; } s.getChars(0, length, buffer, 0); characters(buffer, 0, length); } /** * Writes a GML2 coordinates element * * @param cs * @throws SAXException */ public void coordinates(CoordinateSequence cs) throws SAXException { startElement(coordinates, null); coordinates(cs, ',', ' ', sb); characters(sb); endElement(coordinates); } /** * Writes a single x/y position, without wrapping it in any element * * @param x * @param y * @throws SAXException */ public void position(double x, double y, double z) throws SAXException { position(x, y, z, sb); characters(sb); } void position(double x, double y, double z, StringBuffer sb) { sb.setLength(0); appendDecimal(x); if (!Double.isNaN(y)) { sb.append(" "); appendDecimal(y); } if (!Double.isNaN(z)) { sb.append(" "); appendDecimal(z); } } void positions(CoordinateSequence coordinates) { coordinates(coordinates, ' ', ' ', sb); } void coordinates(CoordinateSequence coordinates, char cs, char ts, StringBuffer sb) { sb.setLength(0); int n = coordinates.size(); int dim = CoordinateSequences.coordinateDimension(coordinates); for (int i = 0; i < n; i++) { appendDecimal(coordinates.getX(i)).append(cs); appendDecimal(coordinates.getY(i)); if(dim == 3) { sb.append(cs); appendDecimal(coordinates.getOrdinate(i, 2)); } sb.append(ts); } sb.setLength(sb.length() - 1); } /** * Writes a single ordinate, without wrapping it inside any element * * @param x * @throws SAXException */ public void ordinate(double x) throws SAXException { sb.setLength(0); appendDecimal(x); characters(sb); } private StringBuffer appendDecimal(double x) { if ((Math.abs(x) >= DECIMAL_MIN && x < DECIMAL_MAX) || x == 0) { x = truncate(x); long lx = (long) x; if (lx == x) sb.append(lx); else sb.append(x); } else { if (forceDecimal) { coordFormatter.format(x, sb, ZERO); } else { sb.append(truncate(x)); } } return sb; } final double truncate(double x) { return Math.floor(x * scale + 0.5) / scale; } /** * Write a GML3 posList * * @param coordinateSequence * @throws SAXException */ public void posList(CoordinateSequence coordinateSequence) throws SAXException { startElement(posList, null); positions(coordinateSequence); characters(sb); endElement(posList); } }