001/* *********************************************************************** * 002 * project: org.matsim.* 003 * KMZWriter.java 004 * * 005 * *********************************************************************** * 006 * * 007 * copyright : (C) 2007 by the members listed in the COPYING, * 008 * LICENSE and WARRANTY file. * 009 * email : info at matsim dot org * 010 * * 011 * *********************************************************************** * 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; either version 2 of the License, or * 016 * (at your option) any later version. * 017 * See also COPYING, LICENSE and WARRANTY file * 018 * * 019 * *********************************************************************** */ 020 021package org.matsim.vis.kml; 022 023import java.io.BufferedWriter; 024import java.io.FileInputStream; 025import java.io.FileOutputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.OutputStreamWriter; 029import java.util.HashMap; 030import java.util.Map; 031import java.util.zip.ZipEntry; 032import java.util.zip.ZipOutputStream; 033 034import javax.xml.bind.JAXBContext; 035import javax.xml.bind.JAXBException; 036import javax.xml.bind.Marshaller; 037 038import org.apache.log4j.Level; 039import org.apache.log4j.Logger; 040import org.matsim.core.api.internal.MatsimSomeWriter; 041 042import net.opengis.kml.v_2_2_0.KmlType; 043import net.opengis.kml.v_2_2_0.LinkType; 044import net.opengis.kml.v_2_2_0.NetworkLinkType; 045import net.opengis.kml.v_2_2_0.ObjectFactory; 046 047/** 048 * A writer for complex keyhole markup files used by Google Earth. It supports 049 * packing multiple kml-files into one zip-compressed file which can directly be 050 * read by Google Earth. The files will have the ending *.kmz. 051 * 052 * @author mrieser 053 * 054 */ 055public class KMZWriter implements MatsimSomeWriter { 056 057 private static final Logger log = Logger.getLogger(KMZWriter.class); 058 059 private BufferedWriter out = null; 060 061 private ZipOutputStream zipOut = null; 062 063 private final Map<String, String> nonKmlFiles = new HashMap<String, String>(); 064 065 private final static Marshaller marshaller; 066 067 private final static ObjectFactory kmlObjectFactory = new ObjectFactory(); 068 069 static { 070 try { 071 JAXBContext jaxbContext = JAXBContext.newInstance("net.opengis.kml.v_2_2_0"); 072 marshaller = jaxbContext.createMarshaller(); 073 marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); 074 } catch (JAXBException e) { 075 throw new RuntimeException(e); 076 } 077 } 078 079 /** 080 * Creates a new kmz-file and a writer for it and opens the file for writing. 081 * 082 * @param outFilename 083 * the location of the file to be written. 084 */ 085 public KMZWriter(final String outFilename) { 086 log.setLevel( Level.INFO ) ; 087 088 String filename = outFilename; 089 if (filename.endsWith(".kml") || filename.endsWith(".kmz")) { 090 filename = filename.substring(0, filename.length() - 4); 091 } 092 093 try { 094 this.zipOut = new ZipOutputStream(new FileOutputStream(filename + ".kmz")); 095 this.out = new BufferedWriter(new OutputStreamWriter(this.zipOut, "UTF8")); 096 } catch (IOException e) { 097 e.printStackTrace(); 098 } 099 100 // generate the first KML entry in the zip file that links to the (later 101 // added) main-KML. 102 // this is required as GoogleEarth will only display the first-added KML in 103 // a kmz. 104 KmlType docKML = kmlObjectFactory.createKmlType(); 105 NetworkLinkType nl = kmlObjectFactory.createNetworkLinkType(); 106 107 LinkType link = kmlObjectFactory.createLinkType(); 108 link.setHref("main.kml"); 109 nl.setLink(link); 110 docKML.setAbstractFeatureGroup(kmlObjectFactory.createNetworkLink(nl)); 111 112 writeKml("doc.kml", docKML); 113 } 114 115 /** 116 * Adds the specified KML-object to the file. 117 * 118 * @param filename 119 * The internal filename of this kml-object in the kmz-file. Other 120 * kml-objects in the same kmz-file can reference this kml with the 121 * specified filename. 122 * @param kml 123 * The KML-object to store in the file. 124 */ 125 public void writeLinkedKml(final String filename, final KmlType kml) { 126 if (filename.equals("doc.kml")) { 127 throw new IllegalArgumentException( 128 "The filename 'doc.kml' is reserved for the primary kml."); 129 } 130 if (filename.equals("main.kml")) { 131 throw new IllegalArgumentException( 132 "The filename 'main.kml' is reserved for the main kml."); 133 } 134 writeKml(filename, kml); 135 } 136 137 /** 138 * Writes the specified KML-object as the main kml into the file. The main kml 139 * is the one Google Earth reads when the file is opened. It should contain 140 * NetworkLinks to the other KMLs stored in the same file. 141 * 142 * @param kml 143 * the KML-object that will be read by Google Earth when opening the 144 * file. 145 */ 146 public void writeMainKml(final KmlType kml) { 147 writeKml("main.kml", kml); 148 } 149 150 /** 151 * Closes this file for writing. 152 */ 153 public void close() { 154 try { 155 this.out.close(); 156 } catch (IOException e) { 157 e.printStackTrace(); 158 } 159 } 160 161 /** 162 * Adds a file to the kmz which is not a kml file. 163 * 164 * @param filename the path to the file, relative or absolute 165 * @param inZipFilename the filename used for the file in the kmz file 166 * @throws IOException 167 */ 168 public void addNonKMLFile(final String filename, final String inZipFilename) throws IOException { 169 if (this.nonKmlFiles.containsKey(filename) && (inZipFilename.compareTo(this.nonKmlFiles.get(filename)) == 0)) { 170 log.warn("File: " + filename + " is already included in the kmz as " + inZipFilename); 171 return; 172 } 173 this.nonKmlFiles.put(filename, inZipFilename); 174 FileInputStream fis = new FileInputStream(filename); 175 try { 176 addNonKMLFile(fis, inZipFilename); 177 } finally { 178 fis.close(); 179 } 180 } 181 182 /** 183 * Adds some data as a file to the kmz. The data stream will be closed at the end of the method. 184 * 185 * @param data the data to add to the kmz 186 * @param inZipFilename the filename used for the file in the kmz file 187 * @throws IOException 188 */ 189 public void addNonKMLFile(final InputStream data, final String inZipFilename) throws IOException { 190 try { 191 // Allocate a buffer for reading the input files. 192 byte[] buffer = new byte[4096]; 193 int bytesRead; 194 // Create a zip entry and add it to the zip. 195 ZipEntry entry = new ZipEntry(inZipFilename); 196 this.zipOut.putNextEntry(entry); 197 198 // Read the file the file and write it to the zip. 199 while ((bytesRead = data.read(buffer)) != -1) { 200 this.zipOut.write(buffer, 0, bytesRead); 201 } 202 log.debug(entry.getName() + " added to kmz."); 203 } finally { 204 data.close(); 205 } 206 } 207 208 /** 209 * Adds a file (in form of a byte array) to a kml file. 210 * @param data 211 * @param inZipFilename inZipFilename the filename used for the file in the kmz file 212 * @throws IOException 213 */ 214 public void addNonKMLFile(final byte[] data, final String inZipFilename) throws IOException { 215 // Create a zip entry and add it to the zip. 216 ZipEntry entry = new ZipEntry(inZipFilename); 217 this.zipOut.putNextEntry(entry); 218 this.zipOut.write(data); 219 log.debug(entry.getName() + " added to kmz."); 220 } 221 222 /** 223 * internal routine that does the real writing of the data 224 * 225 * @param filename 226 * @param kml 227 */ 228 private void writeKml(final String filename, final KmlType kml) { 229 try { 230 ZipEntry ze = new ZipEntry(filename); 231 ze.setMethod(ZipEntry.DEFLATED); 232 this.zipOut.putNextEntry(ze); 233 234 try { 235 marshaller.marshal(kmlObjectFactory.createKml(kml), out); 236 } catch (JAXBException e) { 237 e.printStackTrace(); 238 } 239 240 this.out.flush(); 241 242 } catch (IOException e) { 243 e.printStackTrace(); 244 } 245 } 246 247}