1 package tim.prune.save;
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.Dimension;
6 import java.awt.FlowLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.image.BufferedImage;
11 import java.io.FileOutputStream;
12 import java.io.IOException;
13 import java.io.OutputStreamWriter;
14 import java.io.Writer;
15 import java.util.Iterator;
16 import java.util.zip.ZipEntry;
17 import java.util.zip.ZipOutputStream;
19 import javax.imageio.ImageIO;
20 import javax.imageio.ImageWriter;
21 import javax.swing.Box;
22 import javax.swing.BoxLayout;
23 import javax.swing.ImageIcon;
24 import javax.swing.JButton;
25 import javax.swing.JCheckBox;
26 import javax.swing.JDialog;
27 import javax.swing.JFileChooser;
28 import javax.swing.JFrame;
29 import javax.swing.JLabel;
30 import javax.swing.JOptionPane;
31 import javax.swing.JPanel;
32 import javax.swing.JProgressBar;
33 import javax.swing.JTextField;
34 import javax.swing.SwingConstants;
35 import javax.swing.filechooser.FileFilter;
37 import tim.prune.I18nManager;
38 import tim.prune.data.Coordinate;
39 import tim.prune.data.DataPoint;
40 import tim.prune.data.Track;
41 import tim.prune.data.TrackInfo;
42 import tim.prune.gui.ImageUtils;
45 * Class to export track information
46 * into a specified Kml file
48 public class KmlExporter implements Runnable
50 private JFrame _parentFrame = null;
51 private TrackInfo _trackInfo = null;
52 private Track _track = null;
53 private JDialog _dialog = null;
54 private JTextField _descriptionField = null;
55 private JCheckBox _kmzCheckbox = null;
56 private JCheckBox _exportImagesCheckbox = null;
57 private JProgressBar _progressBar = null;
58 private JFileChooser _fileChooser = null;
59 private File _exportFile = null;
61 // Filename of Kml file within zip archive
62 private static final String KML_FILENAME_IN_KMZ = "doc.kml";
63 // Width and height of thumbnail images in Kmz
64 private static final int THUMBNAIL_WIDTH = 240;
65 private static final int THUMBNAIL_HEIGHT = 180;
69 * Constructor giving frame and track
70 * @param inParentFrame parent frame
71 * @param inTrackInfo track info object to save
73 public KmlExporter(JFrame inParentFrame, TrackInfo inTrackInfo)
75 _parentFrame = inParentFrame;
76 _trackInfo = inTrackInfo;
77 _track = inTrackInfo.getTrack();
82 * Show the dialog to select options and export file
84 public void showDialog()
86 // Make dialog window including whether to compress to kmz (and include pictures) or not
89 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportkml.title"), true);
90 _dialog.setLocationRelativeTo(_parentFrame);
91 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
92 _dialog.getContentPane().add(makeDialogComponents());
96 _progressBar.setVisible(false);
102 * Create dialog components
103 * @return Panel containing all gui elements in dialog
105 private Component makeDialogComponents()
107 JPanel dialogPanel = new JPanel();
108 dialogPanel.setLayout(new BorderLayout());
109 JPanel mainPanel = new JPanel();
110 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
111 // Make a central panel with the text box and checkboxes
112 JPanel descPanel = new JPanel();
113 descPanel.setLayout(new FlowLayout());
114 descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
115 _descriptionField = new JTextField(20);
116 descPanel.add(_descriptionField);
117 mainPanel.add(descPanel);
118 dialogPanel.add(mainPanel, BorderLayout.CENTER);
119 // Checkboxes for kmz export and image export
120 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
121 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
122 _kmzCheckbox.addActionListener(new ActionListener() {
123 public void actionPerformed(ActionEvent e)
125 // enable image checkbox if kmz activated
129 mainPanel.add(_kmzCheckbox);
130 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
131 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
132 mainPanel.add(_exportImagesCheckbox);
133 mainPanel.add(Box.createVerticalStrut(10));
134 _progressBar = new JProgressBar(0, 100);
135 _progressBar.setVisible(false);
136 mainPanel.add(_progressBar);
137 mainPanel.add(Box.createVerticalStrut(10));
138 // button panel at bottom
139 JPanel buttonPanel = new JPanel();
140 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
141 JButton okButton = new JButton(I18nManager.getText("button.ok"));
142 ActionListener okListener = new ActionListener() {
143 public void actionPerformed(ActionEvent e)
148 okButton.addActionListener(okListener);
149 _descriptionField.addActionListener(okListener);
150 buttonPanel.add(okButton);
151 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
152 cancelButton.addActionListener(new ActionListener() {
153 public void actionPerformed(ActionEvent e)
158 buttonPanel.add(cancelButton);
159 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
165 * Enable the checkboxes according to data
167 private void enableCheckboxes()
169 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
170 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
171 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
176 * Start the export process based on the input parameters
178 private void startExport()
180 // OK pressed, so choose output file
181 if (_fileChooser == null)
182 {_fileChooser = new JFileChooser();}
183 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
184 _fileChooser.setFileFilter(new FileFilter() {
185 public boolean accept(File f)
187 return (f != null && (f.isDirectory()
188 || f.getName().toLowerCase().endsWith(".kml") || f.getName().toLowerCase().endsWith(".kmz")));
190 public String getDescription()
192 return I18nManager.getText("dialog.exportkml.filetype");
195 String requiredExtension = null, otherExtension = null;
196 if (_kmzCheckbox.isSelected())
198 requiredExtension = ".kmz"; otherExtension = ".kml";
202 requiredExtension = ".kml"; otherExtension = ".kmz";
204 _fileChooser.setAcceptAllFileFilterUsed(false);
205 // Allow choose again if an existing file is selected
206 boolean chooseAgain = false;
210 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
212 // OK pressed and file chosen
213 File file = _fileChooser.getSelectedFile();
214 if (file.getName().toLowerCase().endsWith(otherExtension))
216 String path = file.getAbsolutePath();
217 file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
219 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
221 file = new File(file.getAbsolutePath() + requiredExtension);
223 // Check if file exists and if necessary prompt for overwrite
224 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
225 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
226 I18nManager.getText("dialog.save.overwrite.text"),
227 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
228 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
229 == JOptionPane.YES_OPTION)
231 // New file or overwrite confirmed, so initiate export in separate thread
233 new Thread(this).start();
240 } while (chooseAgain);
245 * Run method for controlling separate thread for exporting
249 // Initialise progress bar
250 _progressBar.setVisible(true);
251 _progressBar.setValue(0);
252 boolean exportToKmz = _kmzCheckbox.isSelected();
253 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
254 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
255 OutputStreamWriter writer = null;
256 ZipOutputStream zipOutputStream = null;
259 // Select writer according to whether kmz requested or not
260 if (!_kmzCheckbox.isSelected())
262 // normal writing to file
263 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
267 // kmz requested - need zip output stream
268 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
269 writer = new OutputStreamWriter(zipOutputStream);
270 // Make an entry in the zip file for the kml file
271 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
272 zipOutputStream.putNextEntry(kmlEntry);
275 int numPoints = exportData(writer, exportImages);
276 // update progress bar
277 _progressBar.setValue(1);
279 // close zip entry if necessary
280 if (zipOutputStream != null)
282 // Make sure all buffered data in writer is flushed
284 // Close off this entry in the zip file
285 zipOutputStream.closeEntry();
286 // Export images into zip file too if requested
289 // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
290 exportThumbnails(zipOutputStream);
296 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1")
297 + " " + numPoints + " " + I18nManager.getText("dialog.save.ok2")
298 + " " + _exportFile.getAbsolutePath(),
299 I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE);
300 // export successful so need to close dialog and return
304 catch (IOException ioe)
306 // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
308 if (writer != null) writer.close();
310 catch (IOException ioe2) {}
311 JOptionPane.showMessageDialog(_parentFrame,
312 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
313 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
315 // if not returned already, export failed so need to recall the file selection
321 * Export the information to the given writer
322 * @param inWriter writer object
323 * @param inExportImages true if image thumbnails are to be referenced
324 * @return number of points written
326 private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
329 // TODO: Look at segments of track, and split into separate lines in Kml if necessary
330 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
331 inWriter.write("\t<name>");
332 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
334 inWriter.write(_descriptionField.getText());
338 inWriter.write("Export from Prune");
340 inWriter.write("</name>\n");
343 DataPoint point = null;
344 boolean hasTrackpoints = false;
345 // Loop over waypoints
346 boolean writtenPhotoHeader = false;
347 int numPoints = _track.getNumPoints();
349 for (i=0; i<numPoints; i++)
351 point = _track.getPoint(i);
352 // Make a blob for each waypoint
353 if (point.isWaypoint())
355 exportWaypoint(point, inWriter);
357 // Make a blob with description for each photo
358 if (point.getPhoto() != null)
360 if (!writtenPhotoHeader)
362 inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
363 writtenPhotoHeader = true;
366 exportPhotoPoint(point, inWriter, inExportImages, photoNum);
370 hasTrackpoints = true;
373 // Make a line for the track, if there is one
376 inWriter.write("\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
377 + "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
378 + "\t\t</Style>\n\t\t<LineString>\n\t\t\t<coordinates>");
379 // Loop over track points
380 for (i=0; i<numPoints; i++)
382 point = _track.getPoint(i);
383 if (!point.isWaypoint())
385 exportTrackpoint(point, inWriter);
388 inWriter.write("\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>");
390 inWriter.write("</Folder>\n</kml>");
396 * Export the specified waypoint into the file
397 * @param inPoint waypoint to export
398 * @param inWriter writer object
399 * @throws IOException on write failure
401 private void exportWaypoint(DataPoint inPoint, Writer inWriter) throws IOException
403 inWriter.write("\t<Placemark>\n\t\t<name>");
404 inWriter.write(inPoint.getWaypointName().trim());
405 inWriter.write("</name>\n");
406 inWriter.write("\t\t<Point>\n\t\t\t<coordinates>");
407 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
409 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
410 inWriter.write(",0</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
415 * Export the specified photo into the file
416 * @param inPoint data point including photo
417 * @param inWriter writer object
418 * @param inImageLink flag to set whether to export image links or not
419 * @param inImageNumber number of image for filename
420 * @throws IOException on write failure
422 private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink, int inImageNumber)
425 inWriter.write("\t<Placemark>\n\t\t<name>");
426 inWriter.write(inPoint.getPhoto().getFile().getName());
427 inWriter.write("</name>\n");
430 // Work out image dimensions of thumbnail
431 Dimension picSize = inPoint.getPhoto().getSize();
432 Dimension thumbSize = ImageUtils.getThumbnailSize(picSize.width, picSize.height, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
433 // Write out some html for the thumbnail images
434 inWriter.write("<description><![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
435 + inImageNumber + ".jpg' width='" + thumbSize.width + "' height='" + thumbSize.height + "'></center></td></tr>"
436 + "<tr><td><center>Caption for the photo</center></td></tr></table>]]></description>");
438 inWriter.write("<styleUrl>#camera_icon</styleUrl>\n");
439 inWriter.write("\t\t<Point>\n\t\t\t<coordinates>");
440 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
442 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
443 inWriter.write(",0</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
448 * Export the specified trackpoint into the file
449 * @param inPoint trackpoint to export
450 * @param inWriter writer object
452 private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
454 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
456 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
457 // Altitude not exported, locked to ground by Google Earth
458 inWriter.write(",0\n");
463 * Loop through the photos and create thumbnails
464 * @param inZipStream zip stream to save image files to
466 private void exportThumbnails(ZipOutputStream inZipStream) throws IOException
468 // set up image writer
469 Iterator writers = ImageIO.getImageWritersByFormatName("jpg");
470 if (writers == null || !writers.hasNext())
472 throw new IOException("no JPEG writer found");
474 ImageWriter imageWriter = (ImageWriter) writers.next();
476 int numPoints = _track.getNumPoints();
477 DataPoint point = null;
479 // Loop over all points in track
480 for (int i=0; i<numPoints; i++)
482 point = _track.getPoint(i);
483 if (point.getPhoto() != null)
486 // Make a new entry in zip file
487 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
488 inZipStream.putNextEntry(entry);
489 // Load image and write to outstream
490 ImageIcon icon = new ImageIcon(point.getPhoto().getFile().getAbsolutePath());
492 // Scale and smooth image to required size
493 Dimension outputSize = ImageUtils.getThumbnailSize(
494 point.getPhoto().getWidth(), point.getPhoto().getHeight(),
495 THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
496 BufferedImage bufferedImage = ImageUtils.createScaledImage(icon.getImage(), outputSize.width, outputSize.height);
498 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
499 imageWriter.write(bufferedImage);
500 // Close zip file entry
501 inZipStream.closeEntry();
502 // Update progress bar
503 _progressBar.setValue(photoNum+1);
510 * @return number of correlated photos in the track
512 private int getNumPhotosToExport()
514 int numPoints = _track.getNumPoints();
516 DataPoint point = null;
517 // Loop over all points in track
518 for (int i=0; i<numPoints; i++)
520 point = _track.getPoint(i);
521 if (point.getPhoto() != null)