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;
36 import tim.prune.Config;
37 import tim.prune.I18nManager;
38 import tim.prune.UpdateMessageBroker;
39 import tim.prune.data.Altitude;
40 import tim.prune.data.Coordinate;
41 import tim.prune.data.DataPoint;
42 import tim.prune.data.Field;
43 import tim.prune.data.Track;
44 import tim.prune.data.TrackInfo;
45 import tim.prune.gui.ImageUtils;
46 import tim.prune.load.GenericFileFilter;
49 * Class to export track information
50 * into a specified Kml file
52 public class KmlExporter implements Runnable
54 private JFrame _parentFrame = null;
55 private TrackInfo _trackInfo = null;
56 private Track _track = null;
57 private JDialog _dialog = null;
58 private JTextField _descriptionField = null;
59 private JCheckBox _altitudesCheckbox = null;
60 private JCheckBox _kmzCheckbox = null;
61 private JCheckBox _exportImagesCheckbox = null;
62 private JProgressBar _progressBar = null;
63 private JFileChooser _fileChooser = null;
64 private File _exportFile = null;
66 // Filename of Kml file within zip archive
67 private static final String KML_FILENAME_IN_KMZ = "doc.kml";
68 // Width and height of thumbnail images in Kmz
69 private static final int THUMBNAIL_WIDTH = 240;
70 private static final int THUMBNAIL_HEIGHT = 180;
74 * Constructor giving frame and track
75 * @param inParentFrame parent frame
76 * @param inTrackInfo track info object to save
78 public KmlExporter(JFrame inParentFrame, TrackInfo inTrackInfo)
80 _parentFrame = inParentFrame;
81 _trackInfo = inTrackInfo;
82 _track = inTrackInfo.getTrack();
87 * Show the dialog to select options and export file
89 public void showDialog()
91 // Make dialog window including whether to compress to kmz (and include pictures) or not
94 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportkml.title"), true);
95 _dialog.setLocationRelativeTo(_parentFrame);
96 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
97 _dialog.getContentPane().add(makeDialogComponents());
101 _progressBar.setVisible(false);
107 * Create dialog components
108 * @return Panel containing all gui elements in dialog
110 private Component makeDialogComponents()
112 JPanel dialogPanel = new JPanel();
113 dialogPanel.setLayout(new BorderLayout());
114 JPanel mainPanel = new JPanel();
115 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
116 // Make a central panel with the text box and checkboxes
117 JPanel descPanel = new JPanel();
118 descPanel.setLayout(new FlowLayout());
119 descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
120 _descriptionField = new JTextField(20);
121 descPanel.add(_descriptionField);
122 mainPanel.add(descPanel);
123 dialogPanel.add(mainPanel, BorderLayout.CENTER);
124 // Checkbox for altitude export
125 _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
126 _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
127 mainPanel.add(_altitudesCheckbox);
128 // Checkboxes for kmz export and image export
129 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
130 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
131 _kmzCheckbox.addActionListener(new ActionListener() {
132 public void actionPerformed(ActionEvent e)
134 // enable image checkbox if kmz activated
138 mainPanel.add(_kmzCheckbox);
139 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
140 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
141 mainPanel.add(_exportImagesCheckbox);
142 mainPanel.add(Box.createVerticalStrut(10));
143 _progressBar = new JProgressBar(0, 100);
144 _progressBar.setVisible(false);
145 mainPanel.add(_progressBar);
146 mainPanel.add(Box.createVerticalStrut(10));
147 // button panel at bottom
148 JPanel buttonPanel = new JPanel();
149 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
150 JButton okButton = new JButton(I18nManager.getText("button.ok"));
151 ActionListener okListener = new ActionListener() {
152 public void actionPerformed(ActionEvent e)
157 okButton.addActionListener(okListener);
158 _descriptionField.addActionListener(okListener);
159 buttonPanel.add(okButton);
160 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
161 cancelButton.addActionListener(new ActionListener() {
162 public void actionPerformed(ActionEvent e)
167 buttonPanel.add(cancelButton);
168 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
174 * Enable the checkboxes according to data
176 private void enableCheckboxes()
178 boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
179 if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
180 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
181 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
182 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
187 * Start the export process based on the input parameters
189 private void startExport()
191 // OK pressed, so choose output file
192 if (_fileChooser == null)
194 _fileChooser = new JFileChooser();
195 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
196 _fileChooser.setFileFilter(new GenericFileFilter("filetype.kmlkmz", new String[] {"kml", "kmz"}));
197 // start from directory in config which should be set
198 File configDir = Config.getWorkingDirectory();
199 if (configDir != null) {_fileChooser.setCurrentDirectory(configDir);}
201 String requiredExtension = null, otherExtension = null;
202 if (_kmzCheckbox.isSelected())
204 requiredExtension = ".kmz"; otherExtension = ".kml";
208 requiredExtension = ".kml"; otherExtension = ".kmz";
210 _fileChooser.setAcceptAllFileFilterUsed(false);
211 // Allow choose again if an existing file is selected
212 boolean chooseAgain = false;
216 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
218 // OK pressed and file chosen
219 File file = _fileChooser.getSelectedFile();
220 if (file.getName().toLowerCase().endsWith(otherExtension))
222 String path = file.getAbsolutePath();
223 file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
225 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
227 file = new File(file.getAbsolutePath() + requiredExtension);
229 // Check if file exists and if necessary prompt for overwrite
230 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
231 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
232 I18nManager.getText("dialog.save.overwrite.text"),
233 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
234 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
235 == JOptionPane.YES_OPTION)
237 // New file or overwrite confirmed, so initiate export in separate thread
239 new Thread(this).start();
246 } while (chooseAgain);
251 * Run method for controlling separate thread for exporting
255 // Initialise progress bar
256 _progressBar.setVisible(true);
257 _progressBar.setValue(0);
258 boolean exportToKmz = _kmzCheckbox.isSelected();
259 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
260 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
261 OutputStreamWriter writer = null;
262 ZipOutputStream zipOutputStream = null;
265 // Select writer according to whether kmz requested or not
266 if (!_kmzCheckbox.isSelected())
268 // normal writing to file
269 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
273 // kmz requested - need zip output stream
274 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
275 writer = new OutputStreamWriter(zipOutputStream);
276 // Make an entry in the zip file for the kml file
277 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
278 zipOutputStream.putNextEntry(kmlEntry);
281 int numPoints = exportData(writer, exportImages);
282 // update progress bar
283 _progressBar.setValue(1);
285 // close zip entry if necessary
286 if (zipOutputStream != null)
288 // Make sure all buffered data in writer is flushed
290 // Close off this entry in the zip file
291 zipOutputStream.closeEntry();
292 // Export images into zip file too if requested
295 // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
296 exportThumbnails(zipOutputStream);
302 // Store directory in config for later
303 Config.setWorkingDirectory(_exportFile.getParentFile());
305 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
306 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
307 + " " + _exportFile.getAbsolutePath());
308 // export successful so need to close dialog and return
312 catch (IOException ioe)
314 // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
316 if (writer != null) writer.close();
318 catch (IOException ioe2) {}
319 JOptionPane.showMessageDialog(_parentFrame,
320 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
321 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
323 // if not returned already, export failed so need to recall the file selection
329 * Export the information to the given writer
330 * @param inWriter writer object
331 * @param inExportImages true if image thumbnails are to be referenced
332 * @return number of points written
334 private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
337 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
338 inWriter.write("\t<name>");
339 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
341 inWriter.write(_descriptionField.getText());
345 inWriter.write("Export from Prune");
347 inWriter.write("</name>\n");
349 boolean exportAltitudes = _altitudesCheckbox.isSelected();
351 DataPoint point = null;
352 boolean hasTrackpoints = false;
353 // Loop over waypoints
354 boolean writtenPhotoHeader = false;
355 int numPoints = _track.getNumPoints();
357 for (i=0; i<numPoints; i++)
359 point = _track.getPoint(i);
360 // Make a blob for each waypoint
361 if (point.isWaypoint())
363 exportWaypoint(point, inWriter, exportAltitudes);
365 else if (point.getPhoto() == null)
367 hasTrackpoints = true;
369 // Make a blob with description for each photo
370 if (point.getPhoto() != null)
372 if (!writtenPhotoHeader)
374 inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
375 writtenPhotoHeader = true;
378 exportPhotoPoint(point, inWriter, inExportImages, photoNum, exportAltitudes);
381 // Make a line for the track, if there is one
384 // Set up strings for start and end of track segment
385 String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
386 + "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
387 + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
388 + "\t\t</Style>\n\t\t<LineString>\n";
389 if (exportAltitudes) {
390 trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
392 trackStart += "\t\t\t<coordinates>";
393 String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
396 inWriter.write(trackStart);
397 // Loop over track points
398 boolean firstTrackpoint = true;
399 for (i=0; i<numPoints; i++)
401 point = _track.getPoint(i);
402 // start new track segment if necessary
403 if (point.getSegmentStart() && !firstTrackpoint) {
404 inWriter.write(trackEnd);
405 inWriter.write(trackStart);
407 if (!point.isWaypoint() && point.getPhoto() == null)
409 exportTrackpoint(point, inWriter, exportAltitudes);
410 firstTrackpoint = false;
414 inWriter.write(trackEnd);
416 inWriter.write("</Folder>\n</kml>");
422 * Export the specified waypoint into the file
423 * @param inPoint waypoint to export
424 * @param inWriter writer object
425 * @param inExportAltitude true to include altitude
426 * @throws IOException on write failure
428 private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
430 inWriter.write("\t<Placemark>\n\t\t<name>");
431 inWriter.write(inPoint.getWaypointName().trim());
432 inWriter.write("</name>\n");
433 inWriter.write("\t\t<Point>\n");
434 if (inExportAltitude && inPoint.hasAltitude()) {
435 inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
437 inWriter.write("\t\t\t<coordinates>");
438 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
440 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
442 if (inExportAltitude && inPoint.hasAltitude()) {
443 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.FORMAT_METRES));
448 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
453 * Export the specified photo into the file
454 * @param inPoint data point including photo
455 * @param inWriter writer object
456 * @param inImageLink flag to set whether to export image links or not
457 * @param inImageNumber number of image for filename
458 * @param inExportAltitude true to include altitude
459 * @throws IOException on write failure
461 private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
462 int inImageNumber, boolean inExportAltitude)
465 inWriter.write("\t<Placemark>\n\t\t<name>");
466 inWriter.write(inPoint.getPhoto().getFile().getName());
467 inWriter.write("</name>\n");
470 // Work out image dimensions of thumbnail
471 Dimension picSize = inPoint.getPhoto().getSize();
472 Dimension thumbSize = ImageUtils.getThumbnailSize(picSize.width, picSize.height, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
473 // Write out some html for the thumbnail images
474 inWriter.write("<description><![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
475 + inImageNumber + ".jpg' width='" + thumbSize.width + "' height='" + thumbSize.height + "'></center></td></tr>"
476 + "<tr><td><center>Caption for the photo</center></td></tr></table>]]></description>");
478 inWriter.write("<styleUrl>#camera_icon</styleUrl>\n");
479 inWriter.write("\t\t<Point>\n");
480 if (inExportAltitude && inPoint.hasAltitude()) {
481 inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
483 inWriter.write("\t\t\t<coordinates>");
484 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
486 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
488 if (inExportAltitude && inPoint.hasAltitude()) {
489 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.FORMAT_METRES));
494 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
499 * Export the specified trackpoint into the file
500 * @param inPoint trackpoint to export
501 * @param inWriter writer object
502 * @param inExportAltitude true to include altitude
504 private void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
506 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
508 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
509 // Altitude either absolute or locked to ground by Google Earth
511 if (inExportAltitude && inPoint.hasAltitude()) {
512 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.FORMAT_METRES));
517 inWriter.write("\n");
522 * Loop through the photos and create thumbnails
523 * @param inZipStream zip stream to save image files to
525 private void exportThumbnails(ZipOutputStream inZipStream) throws IOException
527 // set up image writer
528 Iterator writers = ImageIO.getImageWritersByFormatName("jpg");
529 if (writers == null || !writers.hasNext())
531 throw new IOException("no JPEG writer found");
533 ImageWriter imageWriter = (ImageWriter) writers.next();
535 int numPoints = _track.getNumPoints();
536 DataPoint point = null;
538 // Loop over all points in track
539 for (int i=0; i<numPoints; i++)
541 point = _track.getPoint(i);
542 if (point.getPhoto() != null)
545 // Make a new entry in zip file
546 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
547 inZipStream.putNextEntry(entry);
548 // Load image and write to outstream
549 ImageIcon icon = new ImageIcon(point.getPhoto().getFile().getAbsolutePath());
551 // Scale and smooth image to required size
552 Dimension outputSize = ImageUtils.getThumbnailSize(
553 point.getPhoto().getWidth(), point.getPhoto().getHeight(),
554 THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
555 BufferedImage bufferedImage = ImageUtils.createScaledImage(icon.getImage(), outputSize.width, outputSize.height);
557 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
558 imageWriter.write(bufferedImage);
559 // Close zip file entry
560 inZipStream.closeEntry();
561 // Update progress bar
562 _progressBar.setValue(photoNum+1);
569 * @return number of correlated photos in the track
571 private int getNumPhotosToExport()
573 int numPoints = _track.getNumPoints();
575 DataPoint point = null;
576 // Loop over all points in track
577 for (int i=0; i<numPoints; i++)
579 point = _track.getPoint(i);
580 if (point.getPhoto() != null)