Class KMLWriter

  1 /*
  2  * Copyright (c) 2016 Vivid Solutions.
  3  *
  4  * All rights reserved. This program and the accompanying materials
  5  * are made available under the terms of the Eclipse Public License 2.0
  6  * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
  7  * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
  8  * and the Eclipse Distribution License is available at
  9  *
 10  * http://www.eclipse.org/org/documents/edl-v10.php.
 11  */
 12  
 13 package org.locationtech.jts.io.kml;
 14  
 15 import java.io.IOException;
 16 import java.io.Writer;
 17 import java.text.DecimalFormat;
 18 import java.text.DecimalFormatSymbols;
 19  
 20 import org.locationtech.jts.geom.Coordinate;
 21 import org.locationtech.jts.geom.Geometry;
 22 import org.locationtech.jts.geom.GeometryCollection;
 23 import org.locationtech.jts.geom.LineString;
 24 import org.locationtech.jts.geom.LinearRing;
 25 import org.locationtech.jts.geom.Point;
 26 import org.locationtech.jts.geom.Polygon;
 27 import org.locationtech.jts.util.StringUtil;
 28  
 29  
 30 /**
 31  * Writes a formatted string containing the KML representation of a JTS
 32  * {@link Geometry}. 
 33  * The output is KML fragments which 
 34  * can be substituted wherever the KML <i>Geometry</i> abstract element can be used.
 35  * <p>
 36  * Output elements are indented to provide a
 37  * nicely-formatted representation. 
 38  * An output line prefix and maximum
 39  * number of coordinates per line can be specified.
 40  * <p>
 41  * The Z ordinate value output can be forced to be a specific value. 
 42  * The <code>extrude</code> and <code>altitudeMode</code> modes can be set. 
 43  * If set, the corresponding sub-elements will be output.
 44  */
 45 public class KMLWriter 
 46 {
 47   /**
 48    * The KML standard value <code>clampToGround</code> for use in {@link #setAltitudeMode(String)}.
 49    */
 50   public static String ALTITUDE_MODE_CLAMPTOGROUND = "clampToGround ";
 51   /**
 52    * The KML standard value <code>relativeToGround</code> for use in {@link #setAltitudeMode(String)}.
 53    */
 54   public static String ALTITUDE_MODE_RELATIVETOGROUND  = "relativeToGround  ";
 55   /**
 56    * The KML standard value <code>absolute</code> for use in {@link #setAltitudeMode(String)}.
 57    */
 58   public static String ALTITUDE_MODE_ABSOLUTE = "absolute";
 59   
 60   /**
 61    * Writes a Geometry as KML to a string, using
 62    * a specified Z value.
 63    * 
 64    * @param geometry the geometry to write
 65    * @param z the Z value to use
 66    * @return a string containing the KML geometry representation
 67    */
 68   public static String writeGeometry(Geometry geometry, double z) {
 69     KMLWriter writer = new KMLWriter();
 70     writer.setZ(z);
 71     return writer.write(geometry);
 72   }
 73  
 74   /**
 75     * Writes a Geometry as KML to a string, using
 76    * a specified Z value, precision, extrude flag,
 77    * and altitude mode code.
 78    * 
 79    * @param geometry the geometry to write
 80    * @param z the Z value to use
 81    * @param precision the maximum number of decimal places to write
 82    * @param extrude the extrude flag to write
 83    * @param altitudeMode the altitude model code to write
 84    * @return a string containing the KML geometry representation
 85    */
 86   public static String writeGeometry(Geometry geometry, double z, int precision,
 87       boolean extrude, String altitudeMode) {
 88     KMLWriter writer = new KMLWriter();
 89     writer.setZ(z);
 90     writer.setPrecision(precision);
 91     writer.setExtrude(extrude);
 92     writer.setAltitudeMode(altitudeMode);
 93     return writer.write(geometry);
 94   }
 95  
 96   private final int INDENT_SIZE = 2;
 97   private static final String COORDINATE_SEPARATOR = ",";
 98   private static final String TUPLE_SEPARATOR = " ";
 99  
100   private String linePrefix = null;
101   private int maxCoordinatesPerLine = 5;
102   private double zVal = Double.NaN;
103   private boolean extrude = false;
104   private boolean tesselate;
105   private String altitudeMode = null;
106   private DecimalFormat numberFormatter = null;
107  
108   /**
109    * Creates a new writer.
110    */
111   public KMLWriter() {
112   }
113  
114   /**
115    * Sets a tag string which is prefixed to every emitted text line.
116    * This can be used to indent the geometry text in a containing document.
117    * 
118    * @param linePrefix the tag string
119    */
120   public void setLinePrefix(String linePrefix) {
121     this.linePrefix = linePrefix;
122   }
123  
124   /**
125    * Sets the maximum number of coordinates to output per line.
126    * 
127    * @param maxCoordinatesPerLine the maximum number of coordinates to output
128    */
129   public void setMaximumCoordinatesPerLine(int maxCoordinatesPerLine) {
130     if (maxCoordinatesPerLine <= 0) {
131       maxCoordinatesPerLine = 1;
132       return;
133     }
134     this.maxCoordinatesPerLine = maxCoordinatesPerLine;
135   }
136  
137   /**
138    * Sets the Z value to be output for all coordinates.
139    * This overrides any Z value present in the Geometry coordinates.
140    * 
141    * @param zVal the Z value to output
142    */
143   public void setZ(double zVal) {
144     this.zVal = zVal;
145   }
146  
147   /**
148    * Sets the flag to be output in the <code>extrude</code> element.
149    * 
150    * @param extrude the extrude flag to output
151    */
152   public void setExtrude(boolean extrude) {
153     this.extrude = extrude;
154   }
155  
156   /**
157    * Sets the flag to be output in the <code>tesselate</code> element.
158    * 
159    * @param tesselate the tesselate flag to output
160    */
161   public void setTesselate(boolean tesselate) {
162     this.tesselate = tesselate;
163   }
164  
165   /**
166    * Sets the value output in the <code>altitudeMode</code> element.
167    * 
168    * @param altitudeMode string representing the altitude mode
169    */
170   public void setAltitudeMode(String altitudeMode) {
171     this.altitudeMode = altitudeMode;
172   }
173  
174   /**
175    * Sets the maximum number of decimal places to output in ordinate values.
176    * Useful for limiting output size.
177    * 
178    * @param precision the number of decimal places to output
179    */
180   public void setPrecision(int precision) {
181     //this.precision = precision;
182     if (precision >= 0)
183       numberFormatter = createFormatter(precision);
184   }
185  
186   /**
187    * Writes a {@link Geometry} in KML format as a string.
188    * 
189    * @param geom the geometry to write
190    * @return a string containing the KML geometry representation
191    */
192   public String write(Geometry geom) {
193     StringBuffer buf = new StringBuffer();
194     write(geom, buf);
195     return buf.toString();
196   }
197  
198   /**
199    * Writes the KML representation of a {@link Geometry} to a {@link Writer}.
200    * 
201    * @param geometry the geometry to write
202    * @param writer the Writer to write to
203    * @throws IOException if an I/O error occurred
204    */
205   public void write(Geometry geometry, Writer writer) throws IOException {
206     writer.write(write(geometry));
207   }
208  
209   /**
210    * Appends the KML representation of a {@link Geometry} to a {@link StringBuffer}.
211    * 
212    * @param geometry the geometry to write
213    * @param buf the buffer to write into
214    */
215   public void write(Geometry geometry, StringBuffer buf) {
216     writeGeometry(geometry, 0, buf);
217   }
218  
219   private void writeGeometry(Geometry g, int level, StringBuffer buf) {
220     String attributes = "";
221     if (g instanceof Point) {
222       writePoint((Point) g, attributes, level, buf);
223     } else if (g instanceof LinearRing) {
224       writeLinearRing((LinearRing) g, attributes, true, level, buf);
225     } else if (g instanceof LineString) {
226       writeLineString((LineString) g, attributes, level, buf);
227     } else if (g instanceof Polygon) {
228       writePolygon((Polygon) g, attributes, level, buf);
229     } else if (g instanceof GeometryCollection) {
230       writeGeometryCollection((GeometryCollection) g, attributes, level, buf);
231     }
232     else 
233       throw new IllegalArgumentException("Geometry type not supported: " + g.getGeometryType());
234   }
235  
236   private void startLine(String text, int level, StringBuffer buf) {
237     if (linePrefix != null)
238       buf.append(linePrefix);
239     buf.append(StringUtil.spaces(INDENT_SIZE * level));
240     buf.append(text);
241   }
242  
243   private String geometryTag(String geometryName, String attributes) {
244     StringBuffer buf = new StringBuffer();
245     buf.append("<");
246     buf.append(geometryName);
247     if (attributes != null && attributes.length() > 0) {
248       buf.append(" ");
249       buf.append(attributes);
250     }
251     buf.append(">");
252     return buf.toString();
253   }
254  
255   private void writeModifiers(int level, StringBuffer buf)
256   {
257     if (extrude) {
258       startLine("<extrude>1</extrude>\n", level, buf);
259     }
260     if (tesselate) {
261       startLine("<tesselate>1</tesselate>\n", level, buf);
262     }
263     if (altitudeMode != null) {
264       startLine("<altitudeMode>" + altitudeMode + "</altitudeMode>\n", level, buf);
265     }
266   }
267   
268   private void writePoint(Point p, String attributes, int level,
269       StringBuffer buf) {
270   // <Point><coordinates>...</coordinates></Point>
271     startLine(geometryTag("Point", attributes) + "\n", level, buf);
272     writeModifiers(level, buf);
273     write(new Coordinate[] { p.getCoordinate() }, level + 1, buf);
274     startLine("</Point>\n", level, buf);
275   }
276  
277   private void writeLineString(LineString ls, String attributes, int level,
278       StringBuffer buf) {
279   // <LineString><coordinates>...</coordinates></LineString>
280     startLine(geometryTag("LineString", attributes) + "\n", level, buf);
281     writeModifiers(level, buf);
282     write(ls.getCoordinates(), level + 1, buf);
283     startLine("</LineString>\n", level, buf);
284   }
285  
286   private void writeLinearRing(LinearRing lr, String attributes, 
287       boolean writeModifiers, int level,
288       StringBuffer buf) {
289   // <LinearRing><coordinates>...</coordinates></LinearRing>
290     startLine(geometryTag("LinearRing", attributes) + "\n", level, buf);
291     if (writeModifiers) writeModifiers(level, buf);
292     write(lr.getCoordinates(), level + 1, buf);
293     startLine("</LinearRing>\n", level, buf);
294   }
295  
296   private void writePolygon(Polygon p, String attributes, int level,
297       StringBuffer buf) {
298     startLine(geometryTag("Polygon", attributes) + "\n", level, buf);
299     writeModifiers(level, buf);
300  
301     startLine("  <outerBoundaryIs>\n", level, buf);
302     writeLinearRing(p.getExteriorRing(), nullfalse, level + 1, buf);
303     startLine("  </outerBoundaryIs>\n", level, buf);
304  
305     for (int t = 0; t < p.getNumInteriorRing(); t++) {
306       startLine("  <innerBoundaryIs>\n", level, buf);
307       writeLinearRing(p.getInteriorRingN(t), nullfalse, level + 1, buf);
308       startLine("  </innerBoundaryIs>\n", level, buf);
309     }
310  
311     startLine("</Polygon>\n", level, buf);
312   }
313  
314   private void writeGeometryCollection(GeometryCollection gc,
315       String attributes, int level, StringBuffer buf) {
316     startLine("<MultiGeometry>\n", level, buf);
317     for (int t = 0; t < gc.getNumGeometries(); t++) {
318       writeGeometry(gc.getGeometryN(t), level + 1, buf);
319     }
320     startLine("</MultiGeometry>\n", level, buf);
321   }
322  
323   /**
324    * Takes a list of coordinates and converts it to KML.<br>
325    * 2d and 3d aware. Terminates the coordinate output with a newline.
326    * 
327    * @param cs array of coordinates
328    */
329   private void write(Coordinate[] coords, int level, StringBuffer buf) {
330     startLine("<coordinates>", level, buf);
331  
332     boolean isNewLine = false;
333     for (int i = 0; i < coords.length; i++) {
334       if (i > 0) {
335         buf.append(TUPLE_SEPARATOR);
336       }
337  
338       if (isNewLine) {
339         startLine("  ", level, buf);
340         isNewLine = false;
341       }
342  
343       write(coords[i], buf);
344  
345       // break output lines to prevent them from getting too long
346       if ((i + 1) % maxCoordinatesPerLine == 0 && i < coords.length - 1) {
347         buf.append("\n");
348         isNewLine = true;
349       }
350     }
351     buf.append("</coordinates>\n");
352   }
353  
354   private void write(Coordinate p, StringBuffer buf) {
355     write(p.x, buf);
356     buf.append(COORDINATE_SEPARATOR);
357     write(p.y, buf);
358  
359     double z = p.getZ();
360     // if altitude was specified directly, use it
361     if (!Double.isNaN(zVal))
362       z = zVal;
363  
364     // only write if Z present
365     // MD - is this right? Or should it always be written?
366     if (!Double.isNaN(z)) {
367       buf.append(COORDINATE_SEPARATOR);
368       write(z, buf);
369     }
370   }
371  
372   private void write(double num, StringBuffer buf) {
373     if (numberFormatter != null)
374       buf.append(numberFormatter.format(num));
375     else
376       buf.append(num);
377   }
378  
379   /**
380    * Creates the <code>DecimalFormat</code> used to write <code>double</code>s
381    * with a sufficient number of decimal places.
382    * 
383    * @param precisionModel
384    *          the <code>PrecisionModel</code> used to determine the number of
385    *          decimal places to write.
386    * @return a <code>DecimalFormat</code> that write <code>double</code> s
387    *         without scientific notation.
388    */
389   private static DecimalFormat createFormatter(int precision) {
390     // specify decimal separator explicitly to avoid problems in other locales
391     DecimalFormatSymbols symbols = new DecimalFormatSymbols();
392     symbols.setDecimalSeparator('.');
393     DecimalFormat format = new DecimalFormat("0."
394         + StringUtil.chars('#', precision), symbols);
395     format.setDecimalSeparatorAlwaysShown(false);
396     return format;
397   }
398  
399 }
400