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}