package net.sourceforge.gpstools.kml;
/* gpxconv
* Copyright (C) 2006 Moritz Ringler
* $Id: GpxKMLWriter.java 441 2010-12-13 20:04:20Z ringler $
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.io.PrintStream;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import net.sourceforge.gpstools.AbstractCommandLine;
import net.sourceforge.gpstools.GPSDings;
import net.sourceforge.gpstools.gpx.GpxType;
import net.sourceforge.gpstools.gpx.Trk;
import net.sourceforge.gpstools.gpx.Trkpt;
import net.sourceforge.gpstools.gpx.Trkseg;
import net.sourceforge.gpstools.gpx.Wpt;
import net.sourceforge.gpstools.utils.ColorProvider;
import net.sourceforge.gpstools.utils.ColorListColorProvider;
import net.sourceforge.gpstools.utils.GpsFormat;
import de.mospace.xml.SaxXMLWriter;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.xml.sax.SAXException;
public class GpxKMLWriter extends GPSDings {
SaxXMLWriter xw;
private final static String LOCAL_URN_PREFIX = "gpsdings:";
private final static String GEARTH_URN_PREFIX = "google:";
private final Properties kmlIcons = new Properties();
private final List<String> myStyles = new Vector<String>();
private final ColorProvider colorGen = new ColorListColorProvider("SBGR");
private final GpsFormat format = GpsFormat.getInstance();
private Set<String> customIcons;
private boolean allowCustomIcons;
public GpxKMLWriter(OutputStream out, String name, boolean enableCustomIcons) throws SAXException, IOException{
allowCustomIcons = enableCustomIcons;
kmlIcons.load(this.getClass().getResourceAsStream("kml.icons.properties"));
try{
xw = new SaxXMLWriter(out, "UTF-8", "http://earth.google.com/kml/2.1");
} catch (UnsupportedEncodingException wonthappen){
throw new Error(wonthappen);
}
xw.startElement("Document");
xw.textElement("name", name);
xw.textElement("visibility", "1");
xw.attribute("id", "waypoint");
xw.startElement("Style");
writeIcon("root://icons/palette-4.png", "32", "128", "32", "32", null);
xw.endElement("Style");
}
private void writeIcon(String href, String x, String y, String w, String h, String scale) throws SAXException, IOException{
xw.startElement("IconStyle");
if(scale != null){
xw.textElement("scale", scale);
}
xw.startElement("Icon");
xw.textElement("href", href);
if(x != null && y!= null){
xw.textElement("x", x);
xw.textElement("y", y);
}
if(w != null && h != null){
xw.textElement("w", w);
xw.textElement("h", h);
}
xw.endElement("Icon");
xw.endElement("IconStyle");
}
public void writeGpx(GpxType gpx) throws SAXException, IOException{
if(gpx.getWptCount() > 0){
/* waypoint styles */
Set<String> syms = new HashSet<String>();
for(Wpt wpt : gpx.getWpt()){
if(wpt.getSym() != null){
syms.add(wpt.getSym().toLowerCase().replaceAll("[^a-z0-9]",""));
}
}
for(String sym : syms){
addSymbolStyle(sym);
}
/* waypoints */
xw.startElement("Folder");
xw.textElement("name", "Waypoints");
xw.textElement("visibility", "1");
for(Wpt pt : gpx.getWpt()){
writeWaypoint(pt);
}
xw.endElement("Folder");
}
if(gpx.getTrkCount() > 0){
/* tracklogs */
xw.startElement("Folder");
xw.textElement("name", "Tracklogs");
xw.textElement("visibility", "1");
for(Trk trk : gpx.getTrk()){
Trkseg[] segs = trk.getTrkseg();
final int numseg = segs.length;
for(int i=0; i<numseg; i++){
writeTrackSegment(trk, segs[i], numseg, i+1);
}
}
xw.endElement("Folder");
}
}
private synchronized void addSymbolStyle(String sym) throws SAXException, IOException{
final String pStyle = "sym." + sym + ".style";
String style = kmlIcons.getProperty(pStyle);
if(myStyles.contains(style)){
return;
}
if(style == null){
/* we have no style for this symbol, before we give up... */
/* try if sym is a single letter or digit:
* if so use gearth icons with 1-9 and a-z */
if(sym.length() == 1){
char c = Character.toLowerCase(sym.charAt(0));
int offset = -1;
style = "letter." + c;
int pal = 5;
if(c > '0' && c < '9'){
style = "digit." + c;
offset = 49;
pal = 3;
} else if(c >= 'a' && c < 'i'){
offset = 49;
} else if (c >= 'i' && c < 'q'){
offset = 73;
} else if (c >= 'q' && c < 'y'){
offset = 97;
} else if (c == 'y' || c == 'z'){
offset = 121;
} else {
style = null;
}
final String pIconSrc = "style." + style + ".icon.src";
if(style != null && kmlIcons.getProperty(pIconSrc) == null){
kmlIcons.setProperty(pStyle, style);
kmlIcons.setProperty(pIconSrc,
GEARTH_URN_PREFIX +
"pal" + pal + "/pIcon" + (c - offset) + "l.png");
}
} else {
/* try if we have a style with the same name */
final String pIconSrc = "style." + sym + ".icon.src";
if(kmlIcons.containsKey(pIconSrc)){
style = sym;
kmlIcons.setProperty(pStyle, style);
}
}
} else {
final String pIcon = "style." + style + ".icon";
final String pIconSrc = pIcon + ".src";
final String pHlIcon = "style." + style + ".highlight.icon";
final String pHlIconSrc = pHlIcon + ".src";
String href = resolveURI(kmlIcons.getProperty(pIconSrc));
String hlHref = resolveURI(kmlIcons.getProperty(pHlIconSrc));
if(href == null){
kmlIcons.setProperty(pStyle, "waypoint");
} else {
String styleId = rewriteId(style);
if(hlHref == null){
writeIconStyle(href, pIcon, styleId, null);
} else {
writeIconStyle(href, pIcon, "sn_"+styleId, null);
writeIconStyle(hlHref, pHlIcon, "sh_"+styleId, "1.1");
writeStyleMap(styleId);
}
myStyles.add(style);
}
}
}
private String rewriteId(String id){
char[] chars = id.toCharArray();
int i = 0;
boolean upper = false;
for(int k=0; k < chars.length; k++){
if(chars[k] == '.'){
upper = true;
} else {
chars[i++] = (upper)? Character.toUpperCase(chars[k]) : chars[k];
upper = false;
}
}
return new String(chars, 0, i);
}
private void writeStyleMap(String style) throws SAXException, IOException{
xw.attribute("id", style);
xw.startElement("StyleMap");
xw.startElement("Pair");
xw.textElement("key","normal");
xw.textElement("styleUrl", "#sn_"+style);
xw.endElement("Pair");
xw.startElement("Pair");
xw.textElement("key","highlight");
xw.textElement("styleUrl", "#sh_"+style);
xw.endElement("Pair");
xw.endElement("StyleMap");
}
private void writeIconStyle(String href, String iconProp, String style, String scale) throws SAXException, IOException{
xw.attribute("id", style);
xw.startElement("Style");
writeIcon(href,
kmlIcons.getProperty(iconProp + ".x"),
kmlIcons.getProperty(iconProp + ".y"),
kmlIcons.getProperty(iconProp + ".w"),
kmlIcons.getProperty(iconProp + ".h"),
scale);
xw.endElement("Style");
}
private String resolveURI(String href){
String result = href;
if(href == null){
//return null;
} else if(href.startsWith(GEARTH_URN_PREFIX)){
result = "http://maps.google.com/mapfiles/kml/" + href.substring(GEARTH_URN_PREFIX.length());
} else if(href.startsWith(LOCAL_URN_PREFIX)){
if(allowCustomIcons){
result = "icon/" + href.substring(LOCAL_URN_PREFIX.length());
if(customIcons == null){
customIcons = new HashSet<String>();
}
customIcons.add(result);
} else {
result = null;
}
}
return result;
}
private void writeTrackSegment(Trk trk, Trkseg seg, int numseg, int segidx) throws SAXException, IOException{
String n = trk.getName();
if(n == null){
n = "UNNAMED TRACK";
}
StringBuilder name = new StringBuilder(n.trim());
if(numseg > 1){
name.append("- ").append(segidx);
}
xw.startElement("Placemark");
xw.textElement("name", name.toString());
/* Style */
xw.startElement("Style");
xw.startElement("LineStyle");
xw.textElement("color", colorGen.getNextColorString());
xw.textElement("width", "2");
xw.endElement("LineStyle");
xw.endElement("Style");
/* Data */
xw.startElement("LineString");
xw.textElement("coordinates", formatCoordinates(seg));
xw.endElement("LineString");
xw.endElement("Placemark");
}
private String formatCoordinates(Trkseg seg){
StringBuilder sb = new StringBuilder();
GpsFormat gpsFormat = GpsFormat.getInstance();
for(Trkpt pt : seg.getTrkpt()){
sb.append(gpsFormat.asLatLon(pt.getLon(), pt.getLat()));
Number ele = pt.getEle();
if(ele != null && ele.doubleValue() >= 0.01){
sb.append(',');
sb.append(gpsFormat.asAltitude(ele));
}
sb.append(' ');
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
private void writeWaypoint(Wpt pt) throws SAXException, IOException{
String lon = format.asLatLon(pt.getLon());
String lat = format.asLatLon(pt.getLat());
BigDecimal ele = pt.getEle();
String alt = (ele == null)? null : format.asAltitude(ele);
String sym = pt.getSym();
if(sym != null){
sym = sym.toLowerCase().replaceAll("[^a-z0-9]","");
}
String style = "#" + (
(sym == null)
? "waypoint"
: rewriteId(kmlIcons.getProperty("sym." + sym + ".style", "waypoint"))
);
xw.startElement("Placemark");
String name = pt.getName();
if(name == null){
name = "Waypoint";
}
xw.textElement("name", name);
String desc = pt.getDesc();
if(desc != null){
xw.textElement("description", pt.getDesc());
}
xw.textElement("styleUrl", style);
xw.startElement("LookAt");
xw.textElement("longitude", lon);
xw.textElement("latitude", lat);
xw.textElement("altitude", "0");
xw.textElement("range", "800");
xw.textElement("tilt", "45");
xw.textElement("heading", "0");
xw.endElement("LookAt");
xw.startElement("Point");
StringBuilder coords = new StringBuilder();
coords.append(lon).append(',');
coords.append(lat);
if(alt != null){
coords.append(',').append(alt);
}
xw.textElement("coordinates", coords.toString());
xw.endElement("Point");
xw.endElement("Placemark");
}
public void endDocument() throws SAXException, IOException{
xw.endElement("Document");
xw.endDocument();
}
public static void addIcon(String icon, ZipOutputStream zippo) throws IOException{
final int BUFFSIZE = 2048;
byte[] buffer = new byte[BUFFSIZE];
String res = "/net/sourceforge/gpstools/res/" + icon;
InputStream in = GpxKMLWriter.class.getResourceAsStream(res);
if(in == null){
throw new IOException("Cannot find resource: " + res);
}
try{
zippo.putNextEntry(new ZipEntry(icon));
int count = 0;
while ((count = in.read(buffer, 0, BUFFSIZE)) != -1) {
zippo.write(buffer, 0, count);
}
} finally {
try{
zippo.closeEntry();
} finally {
in.close();
}
}
}
public static void convertFiles(File[] inpt, File outdir, String title, boolean kmz)
throws IOException, SAXException{
GpxType xgpx;
GpxKMLWriter kw;
final String ext = (kmz)? ".kmz" : ".kml";
/* loop over input files */
for(File gpx : inpt){
/* read input */
final FileInputStream in = new FileInputStream(gpx);
try{
xgpx = GPSDings.readGPX(in);
} finally {
in.close();
}
/* create target file and outputstream*/
File todir = (outdir == null)? gpx.getAbsoluteFile().getParentFile() : outdir;
String name = gpx.getName().replaceFirst("\\.gpx$","");
File kml = new File(todir, name + ext);
OutputStream os = new FileOutputStream(kml);
try{
ZipOutputStream zippo = null;
if(kmz){
zippo = new ZipOutputStream(os);
zippo.putNextEntry(new ZipEntry(name + ".kml"));
os = zippo;
}
os = new BufferedOutputStream(os);
/* create the GPXKMLWriter instance and convert gpx to kml */
kw = new GpxKMLWriter(os, (title == null)? name : title, kmz);
kw.writeGpx(xgpx);
kw.endDocument();
if(kmz && zippo != null){
zippo.closeEntry();
/* if we have customIcons, add them to the kmz */
if(kw.customIcons != null){
for (String icon : kw.customIcons){
addIcon(icon, zippo);
}
}
}
} finally{
os.close();
}
}
}
public static void main(String[] argv){
(new KmlCommandLine(argv)).execute();
}
private static class KmlCommandLine extends AbstractCommandLine<GpxKMLWriter>{
private File outdir;
private String title;
private boolean asKMZ = false;
public static final String OPT_NAME = "name";
public static final String OPT_TO_DIR = "todir";
public static final String OPT_KMZ = "kmz";
public KmlCommandLine(String[] argv){
super(null, argv);
}
@Override
public void execute(){
try{
//java.util.Locale.setDefault(java.util.Locale.US);
processArguments();
File[] input = getInputFiles();
if(input == null){
System.exit(1);
}
GpxKMLWriter.convertFiles(input, outdir, title, asKMZ);
} catch (RuntimeException ex){
ex.printStackTrace();
System.err.println(ex);
System.exit(1);
} catch (Exception ex){
System.err.println(ex);
System.exit(1);
}
}
@Override
public void printHelp(PrintStream out){
try{
super.printHelp(out);
cat("gpxkml.txt");
} catch (Exception ex){
throw new Error(ex);
}
}
@Override
protected Options createOptions(){
Options options = super.createOptions();
options.addOption(makeOption(OPT_NAME, File.class, 1, "name"));
options.addOption(makeOption(OPT_TO_DIR, File.class, 1, "directory"));
options.addOption(makeOption('z', OPT_KMZ, Boolean.class, 0, null));
return options;
}
@Override
protected CommandLine processOptions(Options options)
throws org.apache.commons.cli.ParseException,
IOException,
java.text.ParseException
{
CommandLine cl = super.processOptions(options);
char c;
/* name option */
opt = OPT_NAME;
c = opt.charAt(0);
if(cl.hasOption(c)){
title = cl.getOptionValue(c);
}
/* kmz option */
opt = OPT_KMZ;
c = 'z';
if(cl.hasOption(c)){
asKMZ = true;
}
/* todir option */
opt = OPT_TO_DIR;
c = opt.charAt(0);
if(cl.hasOption(c)){
File outputdir = new File(cl.getOptionValue(c));
if(!outputdir.isDirectory() || outputdir.mkdirs()){
throw new java.io.FileNotFoundException("Cannot neither access nor create directory " + outputdir.getAbsolutePath());
}
outdir = outputdir;
}
return cl;
}
}
}