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.JLabel;
29 import javax.swing.JOptionPane;
30 import javax.swing.JPanel;
31 import javax.swing.JProgressBar;
32 import javax.swing.JTextField;
33 import javax.swing.SwingConstants;
36 import tim.prune.Config;
37 import tim.prune.GenericFunction;
38 import tim.prune.I18nManager;
39 import tim.prune.UpdateMessageBroker;
40 import tim.prune.data.Altitude;
41 import tim.prune.data.Coordinate;
42 import tim.prune.data.DataPoint;
43 import tim.prune.data.Field;
44 import tim.prune.data.Track;
45 import tim.prune.data.TrackInfo;
46 import tim.prune.gui.ImageUtils;
47 import tim.prune.load.GenericFileFilter;
50 * Class to export track information
51 * into a specified Kml file
53 public class KmlExporter extends GenericFunction implements Runnable
55 private TrackInfo _trackInfo = null;
56 private Track _track = null;
57 private JDialog _dialog = null;
58 private JTextField _descriptionField = null;
59 private PointTypeSelector _pointTypeSelector = null;
60 private JCheckBox _altitudesCheckbox = null;
61 private JCheckBox _kmzCheckbox = null;
62 private JCheckBox _exportImagesCheckbox = null;
63 private JLabel _progressLabel = null;
64 private JProgressBar _progressBar = null;
65 private JFileChooser _fileChooser = null;
66 private File _exportFile = null;
67 private JButton _okButton = null;
68 private boolean _cancelPressed = false;
70 // Filename of Kml file within zip archive
71 private static final String KML_FILENAME_IN_KMZ = "doc.kml";
72 // Default width and height of thumbnail images in Kmz
73 private static final int DEFAULT_THUMBNAIL_WIDTH = 240;
74 private static final int DEFAULT_THUMBNAIL_HEIGHT = 180;
75 // Actual selected width and height of thumbnail images in Kmz
76 private static int THUMBNAIL_WIDTH = 0;
77 private static int THUMBNAIL_HEIGHT = 0;
82 * @param inApp app object
84 public KmlExporter(App inApp)
87 _trackInfo = inApp.getTrackInfo();
88 _track = _trackInfo.getTrack();
92 public String getNameKey() {
93 return "function.exportkml";
97 * Show the dialog to select options and export file
101 // Make dialog window including whether to compress to kmz (and include pictures) or not
104 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
105 _dialog.setLocationRelativeTo(_parentFrame);
106 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
107 _dialog.getContentPane().add(makeDialogComponents());
111 _descriptionField.setEnabled(true);
112 _okButton.setEnabled(true);
113 _progressLabel.setText("");
114 _progressBar.setVisible(false);
115 _dialog.setVisible(true);
120 * Create dialog components
121 * @return Panel containing all gui elements in dialog
123 private Component makeDialogComponents()
125 JPanel dialogPanel = new JPanel();
126 dialogPanel.setLayout(new BorderLayout(0, 5));
127 JPanel mainPanel = new JPanel();
128 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
129 // Make a central panel with the text box and checkboxes
130 JPanel descPanel = new JPanel();
131 descPanel.setLayout(new FlowLayout());
132 descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
133 _descriptionField = new JTextField(20);
134 descPanel.add(_descriptionField);
135 descPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
136 mainPanel.add(descPanel);
137 dialogPanel.add(mainPanel, BorderLayout.CENTER);
138 // point type selection
139 _pointTypeSelector = new PointTypeSelector();
140 _pointTypeSelector.setAlignmentX(Component.CENTER_ALIGNMENT);
141 mainPanel.add(_pointTypeSelector);
142 // Checkbox for altitude export
143 _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
144 _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
145 _altitudesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
146 mainPanel.add(_altitudesCheckbox);
147 // Checkboxes for kmz export and image export
148 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
149 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
150 _kmzCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
151 _kmzCheckbox.addActionListener(new ActionListener() {
152 public void actionPerformed(ActionEvent e)
154 // enable image checkbox if kmz activated
158 mainPanel.add(_kmzCheckbox);
159 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
160 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
161 _exportImagesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
162 mainPanel.add(_exportImagesCheckbox);
163 mainPanel.add(Box.createVerticalStrut(10));
164 _progressLabel = new JLabel("...");
165 _progressLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
166 mainPanel.add(_progressLabel);
167 _progressBar = new JProgressBar(0, 100);
168 _progressBar.setVisible(false);
169 _progressBar.setAlignmentX(Component.CENTER_ALIGNMENT);
170 mainPanel.add(_progressBar);
171 mainPanel.add(Box.createVerticalStrut(10));
172 // button panel at bottom
173 JPanel buttonPanel = new JPanel();
174 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
175 _okButton = new JButton(I18nManager.getText("button.ok"));
176 ActionListener okListener = new ActionListener() {
177 public void actionPerformed(ActionEvent e)
182 _okButton.addActionListener(okListener);
183 _descriptionField.addActionListener(okListener);
184 buttonPanel.add(_okButton);
185 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
186 cancelButton.addActionListener(new ActionListener() {
187 public void actionPerformed(ActionEvent e)
189 _cancelPressed = true;
193 buttonPanel.add(cancelButton);
194 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
200 * Enable the checkboxes according to data
202 private void enableCheckboxes()
204 _pointTypeSelector.init(_trackInfo);
205 boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
206 if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
207 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
208 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
209 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
214 * Start the export process based on the input parameters
216 private void startExport()
218 // OK pressed, now validate selection checkboxes
219 if (!_pointTypeSelector.getAnythingSelected()) {
222 // Choose output file
223 if (_fileChooser == null)
225 _fileChooser = new JFileChooser();
226 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
227 _fileChooser.setFileFilter(new GenericFileFilter("filetype.kmlkmz", new String[] {"kml", "kmz"}));
228 // start from directory in config which should be set
229 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
230 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
232 String requiredExtension = null, otherExtension = null;
233 if (_kmzCheckbox.isSelected())
235 requiredExtension = ".kmz"; otherExtension = ".kml";
239 requiredExtension = ".kml"; otherExtension = ".kmz";
241 _fileChooser.setAcceptAllFileFilterUsed(false);
242 // Allow choose again if an existing file is selected
243 boolean chooseAgain = false;
247 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
249 // OK pressed and file chosen
250 File file = _fileChooser.getSelectedFile();
251 if (file.getName().toLowerCase().endsWith(otherExtension))
253 String path = file.getAbsolutePath();
254 file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
256 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
258 file = new File(file.getAbsolutePath() + requiredExtension);
260 // Check if file exists and if necessary prompt for overwrite
261 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
262 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
263 I18nManager.getText("dialog.save.overwrite.text"),
264 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
265 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
266 == JOptionPane.YES_OPTION)
268 // New file or overwrite confirmed, so initiate export in separate thread
270 _cancelPressed = false;
271 new Thread(this).start();
278 } while (chooseAgain);
283 * Run method for controlling separate thread for exporting
287 // Disable ok button to stop second go
288 _okButton.setEnabled(false);
289 _descriptionField.setEnabled(false);
290 // Initialise progress indicators
291 _progressLabel.setText(I18nManager.getText("confirm.running"));
292 _progressBar.setVisible(true);
293 _progressBar.setValue(0);
294 boolean exportToKmz = _kmzCheckbox.isSelected();
295 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
296 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
298 // Determine photo thumbnail size from config
299 THUMBNAIL_WIDTH = Config.getConfigInt(Config.KEY_KMZ_IMAGE_WIDTH);
300 if (THUMBNAIL_WIDTH < DEFAULT_THUMBNAIL_WIDTH) {THUMBNAIL_WIDTH = DEFAULT_THUMBNAIL_WIDTH;}
301 THUMBNAIL_HEIGHT = Config.getConfigInt(Config.KEY_KMZ_IMAGE_HEIGHT);
302 if (THUMBNAIL_HEIGHT < DEFAULT_THUMBNAIL_HEIGHT) {THUMBNAIL_HEIGHT = DEFAULT_THUMBNAIL_HEIGHT;}
304 OutputStreamWriter writer = null;
305 ZipOutputStream zipOutputStream = null;
308 // Select writer according to whether kmz requested or not
309 if (!_kmzCheckbox.isSelected())
311 // normal writing to file
312 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
316 // kmz requested - need zip output stream
317 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
318 // Export images into zip file too if requested
321 // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
322 // This is done first so that photo sizes are known for later
323 exportThumbnails(zipOutputStream);
325 writer = new OutputStreamWriter(zipOutputStream);
326 // Make an entry in the zip file for the kml file
327 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
328 zipOutputStream.putNextEntry(kmlEntry);
331 final int numPoints = exportData(writer, exportImages);
332 // update progress bar
333 _progressBar.setValue(1);
335 // close zip entry if necessary
336 if (zipOutputStream != null)
338 // Make sure all buffered data in writer is flushed
340 // Close off this entry in the zip file
341 zipOutputStream.closeEntry();
346 // Store directory in config for later
347 Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
349 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
350 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
351 + " " + _exportFile.getAbsolutePath());
352 // export successful so need to close dialog and return
356 catch (IOException ioe)
358 // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
360 if (writer != null) writer.close();
362 catch (IOException ioe2) {}
363 JOptionPane.showMessageDialog(_parentFrame,
364 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
365 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
367 // if not returned already, export failed so need to recall the file selection
373 * Export the information to the given writer
374 * @param inWriter writer object
375 * @param inExportImages true if image thumbnails are to be referenced
376 * @return number of points written
378 private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
381 boolean writeTrack = _pointTypeSelector.getTrackpointsSelected();
382 boolean writeWaypoints = _pointTypeSelector.getWaypointsSelected();
383 boolean writePhotos = _pointTypeSelector.getPhotopointsSelected();
384 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
385 inWriter.write("\t<name>");
386 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
388 inWriter.write(_descriptionField.getText());
392 inWriter.write("Export from Prune");
394 inWriter.write("</name>\n");
396 boolean absoluteAltitudes = _altitudesCheckbox.isSelected();
398 DataPoint point = null;
399 boolean hasTrackpoints = false;
400 // Loop over waypoints (if any)
401 boolean writtenPhotoHeader = false;
402 final int numPoints = _track.getNumPoints();
405 // Loop over waypoints
406 for (i=0; i<numPoints; i++)
408 point = _track.getPoint(i);
409 // Make a blob for each waypoint
410 if (point.isWaypoint())
412 if (writeWaypoints) {
413 exportWaypoint(point, inWriter, absoluteAltitudes);
417 else if (point.getPhoto() == null)
419 hasTrackpoints = true;
421 // Make a blob with description for each photo
422 // Photos have already been written so picture sizes already known
423 if (point.getPhoto() != null && writePhotos)
425 if (!writtenPhotoHeader)
427 inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
428 writtenPhotoHeader = true;
431 exportPhotoPoint(point, inWriter, inExportImages, photoNum, absoluteAltitudes);
435 // Make a line for the track, if there is one
436 if (hasTrackpoints && writeTrack)
438 // Set up strings for start and end of track segment
439 String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
440 + "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
441 + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
442 + "\t\t</Style>\n\t\t<LineString>\n";
443 if (absoluteAltitudes) {
444 trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
447 trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
449 trackStart += "\t\t\t<coordinates>";
450 String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
453 inWriter.write(trackStart);
454 // Loop over track points
455 boolean firstTrackpoint = true;
456 for (i=0; i<numPoints; i++)
458 point = _track.getPoint(i);
459 // start new track segment if necessary
460 if (point.getSegmentStart() && !firstTrackpoint) {
461 inWriter.write(trackEnd);
462 inWriter.write(trackStart);
464 if (!point.isWaypoint() && point.getPhoto() == null)
466 exportTrackpoint(point, inWriter);
468 firstTrackpoint = false;
472 inWriter.write(trackEnd);
474 inWriter.write("</Folder>\n</kml>");
480 * Export the specified waypoint into the file
481 * @param inPoint waypoint to export
482 * @param inWriter writer object
483 * @param inAbsoluteAltitude true for absolute altitude
484 * @throws IOException on write failure
486 private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
488 inWriter.write("\t<Placemark>\n\t\t<name>");
489 inWriter.write(inPoint.getWaypointName().trim());
490 inWriter.write("</name>\n");
491 inWriter.write("\t\t<Point>\n");
492 if (inAbsoluteAltitude && inPoint.hasAltitude()) {
493 inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
496 inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
498 inWriter.write("\t\t\t<coordinates>");
499 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
501 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
503 if (inPoint.hasAltitude()) {
504 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
509 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
514 * Export the specified photo into the file
515 * @param inPoint data point including photo
516 * @param inWriter writer object
517 * @param inImageLink flag to set whether to export image links or not
518 * @param inImageNumber number of image for filename
519 * @param inAbsoluteAltitude true for absolute altitudes
520 * @throws IOException on write failure
522 private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
523 int inImageNumber, boolean inAbsoluteAltitude)
526 inWriter.write("\t<Placemark>\n\t\t<name>");
527 inWriter.write(inPoint.getPhoto().getFile().getName());
528 inWriter.write("</name>\n");
531 // Work out image dimensions of thumbnail
532 Dimension picSize = inPoint.getPhoto().getSize();
533 Dimension thumbSize = ImageUtils.getThumbnailSize(picSize.width, picSize.height, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
534 // Write out some html for the thumbnail images
535 inWriter.write("<description><![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
536 + inImageNumber + ".jpg' width='" + thumbSize.width + "' height='" + thumbSize.height + "'></center></td></tr>"
537 + "<tr><td><center>Caption for the photo</center></td></tr></table>]]></description>");
539 inWriter.write("<styleUrl>#camera_icon</styleUrl>\n");
540 inWriter.write("\t\t<Point>\n");
541 if (inAbsoluteAltitude && inPoint.hasAltitude()) {
542 inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
545 inWriter.write("\t\t\t<altitudeMode>clampToGround</altitudeMode>\n");
547 inWriter.write("\t\t\t<coordinates>");
548 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
550 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
552 // Altitude if point has one
553 if (inPoint.hasAltitude()) {
554 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
559 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
564 * Export the specified trackpoint into the file
565 * @param inPoint trackpoint to export
566 * @param inWriter writer object
568 private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
570 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
572 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
573 // Altitude if point has one
575 if (inPoint.hasAltitude()) {
576 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
581 inWriter.write("\n");
586 * Loop through the photos and create thumbnails
587 * @param inZipStream zip stream to save image files to
589 private void exportThumbnails(ZipOutputStream inZipStream) throws IOException
591 // set up image writer
592 Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
593 if (writers == null || !writers.hasNext())
595 throw new IOException("no JPEG writer found");
597 ImageWriter imageWriter = writers.next();
599 int numPoints = _track.getNumPoints();
600 DataPoint point = null;
602 // Loop over all points in track
603 for (int i=0; i<numPoints && !_cancelPressed; i++)
605 point = _track.getPoint(i);
606 if (point.getPhoto() != null)
609 // Make a new entry in zip file
610 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
611 inZipStream.putNextEntry(entry);
612 // Load image and write to outstream
613 ImageIcon icon = new ImageIcon(point.getPhoto().getFile().getAbsolutePath());
615 // Scale and smooth image to required size
616 Dimension outputSize = ImageUtils.getThumbnailSize(
617 point.getPhoto().getWidth(), point.getPhoto().getHeight(),
618 THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
619 BufferedImage bufferedImage = ImageUtils.createScaledImage(icon.getImage(), outputSize.width, outputSize.height);
621 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
622 imageWriter.write(bufferedImage);
623 // Close zip file entry
624 inZipStream.closeEntry();
625 // Update progress bar
626 _progressBar.setValue(photoNum+1);
633 * @return number of correlated photos in the track
635 private int getNumPhotosToExport()
637 int numPoints = _track.getNumPoints();
639 DataPoint point = null;
640 // Loop over all points in track
641 for (int i=0; i<numPoints; i++)
643 point = _track.getPoint(i);
644 if (point.getPhoto() != null) {