001/*
002 *  $URL: svn://svn.webarts.bc.ca/open/trunk/projects/WebARTS/ca/bc/webarts/tools/KmlToGeoJSON.java $
003 *  $Author: tgutwin $
004 *  $Revision: 1052 $
005 *  $Date: 2016-03-16 17:05:08 -0700 (Wed, 16 Mar 2016) $
006*/
007/*
008 *
009 *  Written by Tom Gutwin - WebARTS Design.
010 *  Copyright (C) 2016 WebARTS Design, North Vancouver Canada
011 *  http://www.webarts.ca
012 *
013 *  This program is free software; you can redistribute it and/or modify
014 *  it under the terms of the GNU General Public License as published by
015 *  the Free Software Foundation; version 3 of the License, or
016 *  (at your option) any later version.
017 *
018 *  This program is distributed in the hope that it will be useful,
019 *  but WITHOUT ANY WARRANTY; without_ even the implied warranty of
020 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
021 *  GNU General Public License for more details.
022 *
023 *  You should have received a copy of the GNU General Public License
024 *  along with this program; if not, write to the Free Software
025 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
026*/
027
028package ca.bc.webarts.tools;
029
030import java.io.BufferedOutputStream;
031import java.io.ByteArrayOutputStream;
032import java.io.File;
033import java.io.FileInputStream;
034import java.io.FileOutputStream;
035import java.io.IOException;
036import java.util.ArrayList;
037
038import org.geotools.data.DataUtilities;
039import org.geotools.data.simple.SimpleFeatureCollection;
040import org.geotools.geojson.feature.FeatureJSON;
041import org.geotools.kml.v22.KMLConfiguration;
042import org.geotools.xml.PullParser;
043
044import org.opengis.feature.simple.SimpleFeature;
045
046
047/** Wraps and automates conversion of kml files to GeoJSON format.
048  * It uses <a href="http://www.geotools.org">GeoTools</a>.<br /><b>NOTE:</b>geoJson does not have a <i>standard</i>
049  * way of including styling, so none of the KML stsyles are transfered with this class.
050  * <ul><li>You could use <a href="http://wiki.openstreetmap.org/wiki/Geojson_CSS">Geojson CSS</a> /
051  * SVG styling into the geojson, but its use in the implementation varies.
052  * <br />see:<ul><li><a href="http://geojson.org/geojson-spec.html">http://geojson.org/geojson-spec.html</a></li>
053  * and
054  * <li><a href="http://www.w3.org/TR/SVG/styling.html">http://www.w3.org/TR/SVG/styling.html</a></li></ul>
055  * </li>
056  * <li>another option, is using MapBox styling.</li>
057  * </ul>
058  **/
059public class KmlToGeoJSON extends Object
060{
061  /** the full classname as a String for convenience. **/
062  protected static String CLASSNAME = "ca.bc.webarts.tools.KmlToGeoJSON";  // ca.bc.webarts.widgets.Util.getCurrentClassName();
063
064  /**  A holder for this clients System File Separator.  */
065  public final static String SYSTEM_FILE_SEPERATOR = File.separator;
066
067  /**  A holder for this clients System line termination separator.  */
068  public final static String SYSTEM_LINE_SEPERATOR =
069                                           System.getProperty("line.separator");
070
071  /** Empty constructor. **/
072  public KmlToGeoJSON()
073  { }
074
075
076  /** This Outputs a SimpleFeatureCollection to a GeoJSON String (for easier saving and output).
077    *
078    * @param fc is the feature collection to serialize into a String
079    * @return the GeoJSON string of the SimpleFeatureCollection
080    * @throws IOException when
081  **/
082  public static String featuresToString(SimpleFeatureCollection fc ) throws IOException
083  {
084    ByteArrayOutputStream baOs = new ByteArrayOutputStream();
085    FeatureJSON fjson = new FeatureJSON();
086    fjson.writeFeatureCollection(fc, baOs);
087    return baOs.toString();
088  }
089
090
091  /**
092   *  A simple String token replacement routine. Replaces all occurences of the
093   *  token parameter with the replacement value in the passed in sentence
094   *  parameter.
095   *
096   * @param  sentence     The String to perform the token replacement on
097   * @param  token        the token String to seartch for and replace
098   * @param  replacement  the tokens replacement value
099   * @return              The new token replaced string
100   */
101  public static String tokenReplace(String sentence,
102                                    String token,
103                                    String replacement)
104  {
105    String retVal = "";
106    /*
107    int a = 0;
108    while ((a = sentence.indexOf(token)) > -1)
109    {
110      retVal += sentence.substring(0, a) + replacement;
111      sentence = sentence.substring(a + token.length());
112
113    }
114
115    retVal += sentence;
116    */
117    retVal = sentence.replace(token,replacement);
118    return retVal;
119  }
120
121
122  /** Very basic formatter to make a JSON string more readable.
123  **/
124  public static String formatJsonString(String inStr)
125  {
126    String retVal = "\n"+inStr;
127    System.out.println("\n\nPretty Formatting the JSONString");
128    retVal=tokenReplace(retVal,"{","{"+SYSTEM_LINE_SEPERATOR+"  ");
129    retVal=tokenReplace(retVal,",",","+SYSTEM_LINE_SEPERATOR);
130    retVal=tokenReplace(retVal,"}",SYSTEM_LINE_SEPERATOR+"}");
131    System.out.println(retVal);
132
133    return retVal;
134  }
135
136
137    /** Indents/spaces out an JSON result. **/
138  public static StringBuilder jsonIndenter(String s)
139  {
140    StringBuilder retVal = new StringBuilder("");
141    if (s!=null)
142    {
143      int indent = 0;
144      boolean opening = false;
145      boolean closing = false;
146      boolean lf = false;
147      char [] sbChar = s.toCharArray();
148
149      for (int i=0; i< sbChar.length;i++)
150      {
151        opening = false;
152        closing = false;
153        lf = false;
154        if (sbChar[i]=='}' ) // closing
155        {
156          retVal.append("\n");
157          for (int j=0;j<indent-1;j++) retVal.append("  ");
158          retVal.append(sbChar[i]);
159          indent--; //indent--;
160          if(i+1<sbChar.length && sbChar[i+1]!=',')
161          {
162            if(sbChar[i+1]!=']') indent--;
163            retVal.append("\n");
164            for (int j=0;j<((sbChar[i+1]!=']')?indent:indent-1);j++) retVal.append("  ");
165          }
166        }
167        else if(sbChar[i]=='[')  // opening
168        {
169          indent++;
170          retVal.append("\n");
171          for (int j=0;j<indent-1;j++) retVal.append("  ");
172          retVal.append(sbChar[i]);
173        }
174        else if(sbChar[i]==']')  // closing
175        {
176          indent--;
177          retVal.append(sbChar[i]);
178        }
179        else if(sbChar[i]=='{')  // opening
180        {
181          indent++;
182          retVal.append("\n");
183          for (int j=0;j<indent-1;j++) retVal.append("  ");
184          retVal.append(sbChar[i]);
185          retVal.append("\n");
186          for (int j=0;j<indent;j++) retVal.append("  ");
187        }
188        else if(sbChar[i]==','&&!Character.isDigit(sbChar[i-1]))  // continue  attributes
189        {
190          //indent++;
191          retVal.append(sbChar[i]);
192          retVal.append("\n");
193          for (int j=0;j<indent;j++) retVal.append("  ");
194        }
195        else if (sbChar[i]!='\n')
196        {
197          retVal.append(sbChar[i]);
198        }
199      }
200    }
201    return retVal;
202  }
203
204
205    /** Indents/spaces out an XML result. **/
206  public static StringBuilder responseXMLIndenter(StringBuilder sb)
207  {
208    StringBuilder retVal = new StringBuilder("");
209    if (sb!=null)
210    {
211      int indent = -1;
212      boolean opening = false;
213      boolean closing = false;
214      boolean lf = false;
215      char [] sbChar = sb.toString().toCharArray();
216
217      for (int i=0; i< sbChar.length;i++)
218      {
219        opening = false;
220        closing = false;
221        lf = false;
222        if ((sbChar[i]=='<'&&sbChar[i+1]=='/') )
223        {
224          retVal.append("\n");
225          for (int j=0;j<indent;j++) retVal.append("  ");
226          retVal.append(sbChar[i]);
227          indent--; //indent--;
228        }
229        else if(sbChar[i]=='<')
230        {
231          indent++;
232          retVal.append("\n");
233          for (int j=0;j<indent;j++) retVal.append("  ");
234          retVal.append(sbChar[i]);
235        }
236        else if ((sbChar[i]=='/'&&sbChar[i+1]=='>') )
237        {
238          indent--; //indent--;
239          retVal.append(sbChar[i]);
240        }
241        else if (sbChar[i]=='>')
242        {
243          retVal.append(sbChar[i]);
244          //for (int j=0;j<indent;j++) retVal.append("  ");
245        }
246        else if (sbChar[i]!='\n')
247        {
248          retVal.append(sbChar[i]);
249        }
250      }
251    }
252    return retVal;
253  }
254
255
256  /** Takes a kml file and converts its geo features to a GeoTools SimpleFeatureCollection. Use the
257    * {@link #featuresToString(SimpleFeatureCollection) featuresToString}
258    * method to get a String version of it.
259    *
260    * @param kmlFile is a File to process
261    * @return a SimpleFeatureCollection holding all the geo features
262  **/
263  public static SimpleFeatureCollection convertKmlFile(File kmlFile) throws Exception
264  {
265    FileInputStream reader = new FileInputStream(kmlFile);
266    PullParser parser = new PullParser(new KMLConfiguration(), reader, SimpleFeature.class);
267
268    ArrayList<SimpleFeature> features = new ArrayList<>();
269    SimpleFeature simpleFeature = (SimpleFeature) parser.parse();
270    while (simpleFeature != null)
271    {
272      System.out.println(simpleFeature);
273      features.add(simpleFeature);
274      simpleFeature = (SimpleFeature) parser.parse();
275    }
276    SimpleFeatureCollection fc = DataUtilities.collection(features);
277    return fc;
278  }
279
280
281  /** Takes a kml file and converts it to a GeoJSON file holding all the Geo Features
282    * using GeoTools SimpleFeatureCollection. <br /> It saves the geojson file in the same
283    * dir with the same filename BUT with a ".geojson" extension.<br />
284    * It uses {@link #convertKmlFile(File) convertKmlFile} to do the actual file processing.
285    *
286    * @param kmlDirFile is a directory File to recursivly process
287    **/
288  public static void convertKmlFilesInDir(File kmlDirFile) throws Exception
289  {
290    String fName = "";
291    String kmlFilenameNOExtension = "";
292    String geoJsonFilename = "";
293    if (kmlDirFile!=null && kmlDirFile.isDirectory())
294    {
295      File[] fList = kmlDirFile.listFiles();
296      for( int i=0; i< fList.length; i++)
297      {
298        if (fList[i].isDirectory())
299        {
300          convertKmlFilesInDir(fList[i]);
301        }
302        else
303        {
304          if (fList[i].isFile())
305          {
306            fName = fList[i].getAbsolutePath();
307            if(fName.substring(fName.length()-3).equalsIgnoreCase("kml"))
308            {
309              kmlFilenameNOExtension = fName.substring(0,fName.length()-4);
310              geoJsonFilename = kmlFilenameNOExtension + ".geojson";
311              System.out.println("\n  --> "+ fName+" to "+geoJsonFilename);
312              SimpleFeatureCollection fc = convertKmlFile(fList[i]);
313              String geoJsonStr = featuresToString(fc);
314              writeStringToFile(jsonIndenter(geoJsonStr).toString(), geoJsonFilename);
315            }
316          }
317        }
318      }
319    }
320  }
321
322
323  /**
324   * Abstracts the writing of string to a file.
325   *
326   * @param s is the String to writeout
327   * @param fileName is the file name of the file to write the String into
328   * @return if success.. the full pathed filename is returned else null
329   **/
330  public static String writeStringToFile(String s, String fileName)
331  {return  writeStringToFile( s,  fileName, false);}
332
333
334  /**
335   * Abstracts the writing of string to a (zip) file (Zip NOT IMPLEMENTED YET).
336   *
337   * @param s is the String to writeout
338   * @param fileName is the file name of the file to write the String into
339   * @param zipCompress boolean fall to compress with zip compression
340   * @return if success.. the full pathed filename is returned else null
341   **/
342  public static String writeStringToFile(String s, String fileName, boolean zipCompress)
343  {
344    String retVal = fileName;
345
346    try
347    {
348      // FileWriter was not closing the stream
349      /*
350      FileWriter f = new FileWriter(fileName);
351      f.write(s);
352      f.flush();
353      f.close();
354      f = null;
355      */
356      FileOutputStream fos = new FileOutputStream(fileName);
357      byte[] strBytes = s.getBytes();
358      fos.write(strBytes);
359      fos.flush();
360      fos.close();
361      fos = null;
362      System.gc(); // this is required because a bug in Java won't realease
363    }
364    catch (IOException ioEx)
365    {
366      System.out.println("\nERROR Writing file: "+fileName);
367      retVal = null;
368    }
369
370    return retVal;
371  }
372
373
374  /**
375   * Class main commandLine entry method. <br />It takes <b>one</b> commandline parameter: a filename that points at EITHER a kml
376   *  <b>file</b> OR a <b>directory</b> to recursivly process the kml files.
377   **/
378  public static void main(String[] args)
379  {
380    final String methodName = CLASSNAME + ": main()";
381    KmlToGeoJSON instance = new KmlToGeoJSON();
382
383    if (args.length>0)
384    {
385      try
386      {
387        String kmlFilenameNOExtension = args[0].substring(0,args[0].length()-4);
388        String geoJsonFilename = kmlFilenameNOExtension + ".geojson";
389        //System.out.println("\n  --> "+ args[0]+" to "+geoJsonFilename);
390
391        File cmdFile = new File(args[0]);
392        if (cmdFile.isFile())
393        {
394          SimpleFeatureCollection fc = convertKmlFile(cmdFile);
395          String geoJsonStr = featuresToString(fc);
396          writeStringToFile(jsonIndenter(geoJsonStr).toString(), geoJsonFilename);
397        }
398        else if (cmdFile.isDirectory())
399        {
400          convertKmlFilesInDir(cmdFile);
401        }
402
403
404      }
405      catch (Exception ex)
406      {
407        System.out.println("Puked on the file");
408        ex.printStackTrace();
409      }
410    }
411    else
412      System.out.println("Please enter a kml filename.");
413  }
414
415}