KMLWriter.java

/*
 * Copyright (c) 2016 Vivid Solutions.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */

package org.locationtech.jts.io.kml;

import java.io.IOException;
import java.io.Writer;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.util.StringUtil;


/**
 * Writes a formatted string containing the KML representation of a JTS
 * {@link Geometry}. 
 * The output is KML fragments which 
 * can be substituted wherever the KML <i>Geometry</i> abstract element can be used.
 * <p>
 * Output elements are indented to provide a
 * nicely-formatted representation. 
 * An output line prefix and maximum
 * number of coordinates per line can be specified.
 * <p>
 * The Z ordinate value output can be forced to be a specific value. 
 * The <code>extrude</code> and <code>altitudeMode</code> modes can be set. 
 * If set, the corresponding sub-elements will be output.
 */
public class KMLWriter 
{
  /**
   * The KML standard value <code>clampToGround</code> for use in {@link #setAltitudeMode(String)}.
   */
  public static String ALTITUDE_MODE_CLAMPTOGROUND = "clampToGround ";
  /**
   * The KML standard value <code>relativeToGround</code> for use in {@link #setAltitudeMode(String)}.
   */
  public static String ALTITUDE_MODE_RELATIVETOGROUND  = "relativeToGround  ";
  /**
   * The KML standard value <code>absolute</code> for use in {@link #setAltitudeMode(String)}.
   */
  public static String ALTITUDE_MODE_ABSOLUTE = "absolute";
  
  /**
   * Writes a Geometry as KML to a string, using
   * a specified Z value.
   * 
   * @param geometry the geometry to write
   * @param z the Z value to use
   * @return a string containing the KML geometry representation
   */
  public static String writeGeometry(Geometry geometry, double z) {
    KMLWriter writer = new KMLWriter();
    writer.setZ(z);
    return writer.write(geometry);
  }

  /**
    * Writes a Geometry as KML to a string, using
   * a specified Z value, precision, extrude flag,
   * and altitude mode code.
   * 
   * @param geometry the geometry to write
   * @param z the Z value to use
   * @param precision the maximum number of decimal places to write
   * @param extrude the extrude flag to write
   * @param altitudeMode the altitude model code to write
   * @return a string containing the KML geometry representation
   */
  public static String writeGeometry(Geometry geometry, double z, int precision,
      boolean extrude, String altitudeMode) {
    KMLWriter writer = new KMLWriter();
    writer.setZ(z);
    writer.setPrecision(precision);
    writer.setExtrude(extrude);
    writer.setAltitudeMode(altitudeMode);
    return writer.write(geometry);
  }

  private static final int INDENT_SIZE = 2;
  private static final String COORDINATE_SEPARATOR = ",";
  private static final String TUPLE_SEPARATOR = " ";

  private String linePrefix = null;
  private int maxCoordinatesPerLine = 5;
  private double zVal = Double.NaN;
  private boolean extrude = false;
  private boolean tesselate;
  private String altitudeMode = null;
  private DecimalFormat numberFormatter = null;

  /**
   * Creates a new writer.
   */
  public KMLWriter() {
  }

  /**
   * Sets a tag string which is prefixed to every emitted text line.
   * This can be used to indent the geometry text in a containing document.
   * 
   * @param linePrefix the tag string
   */
  public void setLinePrefix(String linePrefix) {
    this.linePrefix = linePrefix;
  }

  /**
   * Sets the maximum number of coordinates to output per line.
   * 
   * @param maxCoordinatesPerLine the maximum number of coordinates to output
   */
  public void setMaximumCoordinatesPerLine(int maxCoordinatesPerLine) {
    if (maxCoordinatesPerLine <= 0) {
      maxCoordinatesPerLine = 1;
      return;
    }
    this.maxCoordinatesPerLine = maxCoordinatesPerLine;
  }

  /**
   * Sets the Z value to be output for all coordinates.
   * This overrides any Z value present in the Geometry coordinates.
   * 
   * @param zVal the Z value to output
   */
  public void setZ(double zVal) {
    this.zVal = zVal;
  }

  /**
   * Sets the flag to be output in the <code>extrude</code> element.
   * 
   * @param extrude the extrude flag to output
   */
  public void setExtrude(boolean extrude) {
    this.extrude = extrude;
  }

  /**
   * Sets the flag to be output in the <code>tesselate</code> element.
   * 
   * @param tesselate the tesselate flag to output
   */
  public void setTesselate(boolean tesselate) {
    this.tesselate = tesselate;
  }

  /**
   * Sets the value output in the <code>altitudeMode</code> element.
   * 
   * @param altitudeMode string representing the altitude mode
   */
  public void setAltitudeMode(String altitudeMode) {
    this.altitudeMode = altitudeMode;
  }

  /**
   * Sets the maximum number of decimal places to output in ordinate values.
   * Useful for limiting output size.
   * 
   * @param precision the number of decimal places to output
   */
  public void setPrecision(int precision) {
    //this.precision = precision;
    if (precision >= 0)
      numberFormatter = createFormatter(precision);
  }

  /**
   * Writes a {@link Geometry} in KML format as a string.
   * 
   * @param geom the geometry to write
   * @return a string containing the KML geometry representation
   */
  public String write(Geometry geom) {
    StringBuffer buf = new StringBuffer();
    write(geom, buf);
    return buf.toString();
  }

  /**
   * Writes the KML representation of a {@link Geometry} to a {@link Writer}.
   * 
   * @param geometry the geometry to write
   * @param writer the Writer to write to
   * @throws IOException if an I/O error occurred
   */
  public void write(Geometry geometry, Writer writer) throws IOException {
    writer.write(write(geometry));
  }

  /**
   * Appends the KML representation of a {@link Geometry} to a {@link StringBuffer}.
   * 
   * @param geometry the geometry to write
   * @param buf the buffer to write into
   */
  public void write(Geometry geometry, StringBuffer buf) {
    writeGeometry(geometry, 0, buf);
  }

  private void writeGeometry(Geometry g, int level, StringBuffer buf) {
    String attributes = "";
    if (g instanceof Point) {
      writePoint((Point) g, attributes, level, buf);
    } else if (g instanceof LinearRing) {
      writeLinearRing((LinearRing) g, attributes, true, level, buf);
    } else if (g instanceof LineString) {
      writeLineString((LineString) g, attributes, level, buf);
    } else if (g instanceof Polygon) {
      writePolygon((Polygon) g, attributes, level, buf);
    } else if (g instanceof GeometryCollection) {
      writeGeometryCollection((GeometryCollection) g, attributes, level, buf);
    }
    else 
      throw new IllegalArgumentException("Geometry type not supported: " + g.getGeometryType());
  }

  private void startLine(String text, int level, StringBuffer buf) {
    if (linePrefix != null)
      buf.append(linePrefix);
    buf.append(StringUtil.spaces(INDENT_SIZE * level));
    buf.append(text);
  }

  private String geometryTag(String geometryName, String attributes) {
    StringBuffer buf = new StringBuffer();
    buf.append("<");
    buf.append(geometryName);
    if (attributes != null && attributes.length() > 0) {
      buf.append(" ");
      buf.append(attributes);
    }
    buf.append(">");
    return buf.toString();
  }

  private void writeModifiers(int level, StringBuffer buf)
  {
    if (extrude) {
      startLine("<extrude>1</extrude>\n", level, buf);
    }
    if (tesselate) {
      startLine("<tesselate>1</tesselate>\n", level, buf);
    }
    if (altitudeMode != null) {
      startLine("<altitudeMode>" + altitudeMode + "</altitudeMode>\n", level, buf);
    }
  }
  
  private void writePoint(Point p, String attributes, int level,
      StringBuffer buf) {
  // <Point><coordinates>...</coordinates></Point>
    startLine(geometryTag("Point", attributes) + "\n", level, buf);
    writeModifiers(level, buf);
    write(new Coordinate[] { p.getCoordinate() }, level + 1, buf);
    startLine("</Point>\n", level, buf);
  }

  private void writeLineString(LineString ls, String attributes, int level,
      StringBuffer buf) {
  // <LineString><coordinates>...</coordinates></LineString>
    startLine(geometryTag("LineString", attributes) + "\n", level, buf);
    writeModifiers(level, buf);
    write(ls.getCoordinates(), level + 1, buf);
    startLine("</LineString>\n", level, buf);
  }

  private void writeLinearRing(LinearRing lr, String attributes, 
      boolean writeModifiers, int level,
      StringBuffer buf) {
  // <LinearRing><coordinates>...</coordinates></LinearRing>
    startLine(geometryTag("LinearRing", attributes) + "\n", level, buf);
    if (writeModifiers) writeModifiers(level, buf);
    write(lr.getCoordinates(), level + 1, buf);
    startLine("</LinearRing>\n", level, buf);
  }

  private void writePolygon(Polygon p, String attributes, int level,
      StringBuffer buf) {
    startLine(geometryTag("Polygon", attributes) + "\n", level, buf);
    writeModifiers(level, buf);

    startLine("  <outerBoundaryIs>\n", level, buf);
    writeLinearRing(p.getExteriorRing(), null, false, level + 1, buf);
    startLine("  </outerBoundaryIs>\n", level, buf);

    for (int t = 0; t < p.getNumInteriorRing(); t++) {
      startLine("  <innerBoundaryIs>\n", level, buf);
      writeLinearRing(p.getInteriorRingN(t), null, false, level + 1, buf);
      startLine("  </innerBoundaryIs>\n", level, buf);
    }

    startLine("</Polygon>\n", level, buf);
  }

  private void writeGeometryCollection(GeometryCollection gc,
      String attributes, int level, StringBuffer buf) {
    startLine("<MultiGeometry>\n", level, buf);
    for (int t = 0; t < gc.getNumGeometries(); t++) {
      writeGeometry(gc.getGeometryN(t), level + 1, buf);
    }
    startLine("</MultiGeometry>\n", level, buf);
  }

  /**
   * Takes a list of coordinates and converts it to KML.<br>
   * 2d and 3d aware. Terminates the coordinate output with a newline.
   * 
   * @param cs array of coordinates
   */
  private void write(Coordinate[] coords, int level, StringBuffer buf) {
    startLine("<coordinates>", level, buf);

    boolean isNewLine = false;
    for (int i = 0; i < coords.length; i++) {
      if (i > 0) {
        buf.append(TUPLE_SEPARATOR);
      }

      if (isNewLine) {
        startLine("  ", level, buf);
        isNewLine = false;
      }

      write(coords[i], buf);

      // break output lines to prevent them from getting too long
      if ((i + 1) % maxCoordinatesPerLine == 0 && i < coords.length - 1) {
        buf.append("\n");
        isNewLine = true;
      }
    }
    buf.append("</coordinates>\n");
  }

  private void write(Coordinate p, StringBuffer buf) {
    write(p.x, buf);
    buf.append(COORDINATE_SEPARATOR);
    write(p.y, buf);

    double z = p.getZ();
    // if altitude was specified directly, use it
    if (!Double.isNaN(zVal))
      z = zVal;

    // only write if Z present
    // MD - is this right? Or should it always be written?
    if (!Double.isNaN(z)) {
      buf.append(COORDINATE_SEPARATOR);
      write(z, buf);
    }
  }

  private void write(double num, StringBuffer buf) {
    if (numberFormatter != null)
      buf.append(numberFormatter.format(num));
    else
      buf.append(num);
  }

  /**
   * Creates the <code>DecimalFormat</code> used to write <code>double</code>s
   * with a sufficient number of decimal places.
   * 
   * @param precisionModel
   *          the <code>PrecisionModel</code> used to determine the number of
   *          decimal places to write.
   * @return a <code>DecimalFormat</code> that write <code>double</code> s
   *         without scientific notation.
   */
  private static DecimalFormat createFormatter(int precision) {
    // specify decimal separator explicitly to avoid problems in other locales
    DecimalFormatSymbols symbols = new DecimalFormatSymbols();
    symbols.setDecimalSeparator('.');
    DecimalFormat format = new DecimalFormat("0."
        + StringUtil.chars('#', precision), symbols);
    format.setDecimalSeparatorAlwaysShown(false);
    return format;
  }

}