]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/KmlExporter.java
Version 6, October 2008
[GpsPrune.git] / tim / prune / save / KmlExporter.java
1 package tim.prune.save;
2
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;
10 import java.io.File;
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;
18
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
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;
47
48 /**
49  * Class to export track information
50  * into a specified Kml file
51  */
52 public class KmlExporter implements Runnable
53 {
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;
65
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;
71
72
73         /**
74          * Constructor giving frame and track
75          * @param inParentFrame parent frame
76          * @param inTrackInfo track info object to save
77          */
78         public KmlExporter(JFrame inParentFrame, TrackInfo inTrackInfo)
79         {
80                 _parentFrame = inParentFrame;
81                 _trackInfo = inTrackInfo;
82                 _track = inTrackInfo.getTrack();
83         }
84
85
86         /**
87          * Show the dialog to select options and export file
88          */
89         public void showDialog()
90         {
91                 // Make dialog window including whether to compress to kmz (and include pictures) or not
92                 if (_dialog == null)
93                 {
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());
98                         _dialog.pack();
99                 }
100                 enableCheckboxes();
101                 _progressBar.setVisible(false);
102                 _dialog.show();
103         }
104
105
106         /**
107          * Create dialog components
108          * @return Panel containing all gui elements in dialog
109          */
110         private Component makeDialogComponents()
111         {
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)
133                         {
134                                 // enable image checkbox if kmz activated
135                                 enableCheckboxes();
136                         }
137                 });
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)
153                         {
154                                 startExport();
155                         }
156                 };
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)
163                         {
164                                 _dialog.dispose();
165                         }
166                 });
167                 buttonPanel.add(cancelButton);
168                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
169                 return dialogPanel;
170         }
171
172
173         /**
174          * Enable the checkboxes according to data
175          */
176         private void enableCheckboxes()
177         {
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());
183         }
184
185
186         /**
187          * Start the export process based on the input parameters
188          */
189         private void startExport()
190         {
191                 // OK pressed, so choose output file
192                 if (_fileChooser == null)
193                 {
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);}
200                 }
201                 String requiredExtension = null, otherExtension = null;
202                 if (_kmzCheckbox.isSelected())
203                 {
204                         requiredExtension = ".kmz"; otherExtension = ".kml";
205                 }
206                 else
207                 {
208                         requiredExtension = ".kml"; otherExtension = ".kmz";
209                 }
210                 _fileChooser.setAcceptAllFileFilterUsed(false);
211                 // Allow choose again if an existing file is selected
212                 boolean chooseAgain = false;
213                 do
214                 {
215                         chooseAgain = false;
216                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
217                         {
218                                 // OK pressed and file chosen
219                                 File file = _fileChooser.getSelectedFile();
220                                 if (file.getName().toLowerCase().endsWith(otherExtension))
221                                 {
222                                         String path = file.getAbsolutePath();
223                                         file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
224                                 }
225                                 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
226                                 {
227                                         file = new File(file.getAbsolutePath() + requiredExtension);
228                                 }
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)
236                                 {
237                                         // New file or overwrite confirmed, so initiate export in separate thread
238                                         _exportFile = file;
239                                         new Thread(this).start();
240                                 }
241                                 else
242                                 {
243                                         chooseAgain = true;
244                                 }
245                         }
246                 } while (chooseAgain);
247         }
248
249
250         /**
251          * Run method for controlling separate thread for exporting
252          */
253         public void run()
254         {
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;
263                 try
264                 {
265                         // Select writer according to whether kmz requested or not
266                         if (!_kmzCheckbox.isSelected())
267                         {
268                                 // normal writing to file
269                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
270                         }
271                         else
272                         {
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);
279                         }
280                         // write file
281                         int numPoints = exportData(writer, exportImages);
282                         // update progress bar
283                         _progressBar.setValue(1);
284
285                         // close zip entry if necessary
286                         if (zipOutputStream != null)
287                         {
288                                 // Make sure all buffered data in writer is flushed
289                                 writer.flush();
290                                 // Close off this entry in the zip file
291                                 zipOutputStream.closeEntry();
292                                 // Export images into zip file too if requested
293                                 if (exportImages)
294                                 {
295                                         // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
296                                         exportThumbnails(zipOutputStream);
297                                 }
298                         }
299
300                         // close file
301                         writer.close();
302                         // Store directory in config for later
303                         Config.setWorkingDirectory(_exportFile.getParentFile());
304                         // show confirmation
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
309                         _dialog.dispose();
310                         return;
311                 }
312                 catch (IOException ioe)
313                 {
314                         // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
315                         try {
316                                 if (writer != null) writer.close();
317                         }
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);
322                 }
323                 // if not returned already, export failed so need to recall the file selection
324                 startExport();
325         }
326
327
328         /**
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
333          */
334         private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
335         throws IOException
336         {
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(""))
340                 {
341                         inWriter.write(_descriptionField.getText());
342                 }
343                 else
344                 {
345                         inWriter.write("Export from Prune");
346                 }
347                 inWriter.write("</name>\n");
348
349                 boolean exportAltitudes = _altitudesCheckbox.isSelected();
350                 int i = 0;
351                 DataPoint point = null;
352                 boolean hasTrackpoints = false;
353                 // Loop over waypoints
354                 boolean writtenPhotoHeader = false;
355                 int numPoints = _track.getNumPoints();
356                 int photoNum = 0;
357                 for (i=0; i<numPoints; i++)
358                 {
359                         point = _track.getPoint(i);
360                         // Make a blob for each waypoint
361                         if (point.isWaypoint())
362                         {
363                                 exportWaypoint(point, inWriter, exportAltitudes);
364                         }
365                         else if (point.getPhoto() == null)
366                         {
367                                 hasTrackpoints = true;
368                         }
369                         // Make a blob with description for each photo
370                         if (point.getPhoto() != null)
371                         {
372                                 if (!writtenPhotoHeader)
373                                 {
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;
376                                 }
377                                 photoNum++;
378                                 exportPhotoPoint(point, inWriter, inExportImages, photoNum, exportAltitudes);
379                         }
380                 }
381                 // Make a line for the track, if there is one
382                 if (hasTrackpoints)
383                 {
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";
391                         }
392                         trackStart += "\t\t\t<coordinates>";
393                         String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
394
395                         // Start segment
396                         inWriter.write(trackStart);
397                         // Loop over track points
398                         boolean firstTrackpoint = true;
399                         for (i=0; i<numPoints; i++)
400                         {
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);
406                                 }
407                                 if (!point.isWaypoint() && point.getPhoto() == null)
408                                 {
409                                         exportTrackpoint(point, inWriter, exportAltitudes);
410                                         firstTrackpoint = false;
411                                 }
412                         }
413                         // end segment
414                         inWriter.write(trackEnd);
415                 }
416                 inWriter.write("</Folder>\n</kml>");
417                 return numPoints;
418         }
419
420
421         /**
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
427          */
428         private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
429         {
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");
436                 }
437                 inWriter.write("\t\t\t<coordinates>");
438                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
439                 inWriter.write(',');
440                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
441                 inWriter.write(",");
442                 if (inExportAltitude && inPoint.hasAltitude()) {
443                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.FORMAT_METRES));
444                 }
445                 else {
446                         inWriter.write("0");
447                 }
448                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
449         }
450
451
452         /**
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
460          */
461         private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
462                 int inImageNumber, boolean inExportAltitude)
463         throws IOException
464         {
465                 inWriter.write("\t<Placemark>\n\t\t<name>");
466                 inWriter.write(inPoint.getPhoto().getFile().getName());
467                 inWriter.write("</name>\n");
468                 if (inImageLink)
469                 {
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>");
477                 }
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");
482                 }
483                 inWriter.write("\t\t\t<coordinates>");
484                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
485                 inWriter.write(',');
486                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
487                 inWriter.write(",");
488                 if (inExportAltitude && inPoint.hasAltitude()) {
489                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.FORMAT_METRES));
490                 }
491                 else {
492                         inWriter.write("0");
493                 }
494                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
495         }
496
497
498         /**
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
503          */
504         private void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
505         {
506                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
507                 inWriter.write(',');
508                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
509                 // Altitude either absolute or locked to ground by Google Earth
510                 inWriter.write(",");
511                 if (inExportAltitude && inPoint.hasAltitude()) {
512                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.FORMAT_METRES));
513                 }
514                 else {
515                         inWriter.write("0");
516                 }
517                 inWriter.write("\n");
518         }
519
520
521         /**
522          * Loop through the photos and create thumbnails
523          * @param inZipStream zip stream to save image files to
524          */
525         private void exportThumbnails(ZipOutputStream inZipStream) throws IOException
526         {
527                 // set up image writer
528                 Iterator writers = ImageIO.getImageWritersByFormatName("jpg");
529                 if (writers == null || !writers.hasNext())
530                 {
531                         throw new IOException("no JPEG writer found");
532                 }
533                 ImageWriter imageWriter = (ImageWriter) writers.next();
534
535                 int numPoints = _track.getNumPoints();
536                 DataPoint point = null;
537                 int photoNum = 0;
538                 // Loop over all points in track
539                 for (int i=0; i<numPoints; i++)
540                 {
541                         point = _track.getPoint(i);
542                         if (point.getPhoto() != null)
543                         {
544                                 photoNum++;
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());
550
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);
556
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);
563                         }
564                 }
565         }
566
567
568         /**
569          * @return number of correlated photos in the track
570          */
571         private int getNumPhotosToExport()
572         {
573                 int numPoints = _track.getNumPoints();
574                 int numPhotos = 0;
575                 DataPoint point = null;
576                 // Loop over all points in track
577                 for (int i=0; i<numPoints; i++)
578                 {
579                         point = _track.getPoint(i);
580                         if (point.getPhoto() != null)
581                         {
582                                 numPhotos++;
583                         }
584                 }
585                 return numPhotos;
586         }
587 }