]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/KmlExporter.java
Version 7, February 2009
[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.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;
34
35 import tim.prune.App;
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;
48
49 /**
50  * Class to export track information
51  * into a specified Kml file
52  */
53 public class KmlExporter extends GenericFunction implements Runnable
54 {
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
75          * @param inApp app object
76          */
77         public KmlExporter(App inApp)
78         {
79                 super(inApp);
80                 _trackInfo = inApp.getTrackInfo();
81                 _track = _trackInfo.getTrack();
82         }
83
84         /** Get name key */
85         public String getNameKey() {
86                 return "function.exportkml";
87         }
88
89         /**
90          * Show the dialog to select options and export file
91          */
92         public void begin()
93         {
94                 // Make dialog window including whether to compress to kmz (and include pictures) or not
95                 if (_dialog == null)
96                 {
97                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
98                         _dialog.setLocationRelativeTo(_parentFrame);
99                         _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
100                         _dialog.getContentPane().add(makeDialogComponents());
101                         _dialog.pack();
102                 }
103                 enableCheckboxes();
104                 _progressBar.setVisible(false);
105                 _dialog.setVisible(true);
106         }
107
108
109         /**
110          * Create dialog components
111          * @return Panel containing all gui elements in dialog
112          */
113         private Component makeDialogComponents()
114         {
115                 JPanel dialogPanel = new JPanel();
116                 dialogPanel.setLayout(new BorderLayout());
117                 JPanel mainPanel = new JPanel();
118                 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
119                 // Make a central panel with the text box and checkboxes
120                 JPanel descPanel = new JPanel();
121                 descPanel.setLayout(new FlowLayout());
122                 descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
123                 _descriptionField = new JTextField(20);
124                 descPanel.add(_descriptionField);
125                 mainPanel.add(descPanel);
126                 dialogPanel.add(mainPanel, BorderLayout.CENTER);
127                 // Checkbox for altitude export
128                 _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
129                 _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
130                 mainPanel.add(_altitudesCheckbox);
131                 // Checkboxes for kmz export and image export
132                 _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
133                 _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
134                 _kmzCheckbox.addActionListener(new ActionListener() {
135                         public void actionPerformed(ActionEvent e)
136                         {
137                                 // enable image checkbox if kmz activated
138                                 enableCheckboxes();
139                         }
140                 });
141                 mainPanel.add(_kmzCheckbox);
142                 _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
143                 _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
144                 mainPanel.add(_exportImagesCheckbox);
145                 mainPanel.add(Box.createVerticalStrut(10));
146                 _progressBar = new JProgressBar(0, 100);
147                 _progressBar.setVisible(false);
148                 mainPanel.add(_progressBar);
149                 mainPanel.add(Box.createVerticalStrut(10));
150                 // button panel at bottom
151                 JPanel buttonPanel = new JPanel();
152                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
153                 JButton okButton = new JButton(I18nManager.getText("button.ok"));
154                 ActionListener okListener = new ActionListener() {
155                         public void actionPerformed(ActionEvent e)
156                         {
157                                 startExport();
158                         }
159                 };
160                 okButton.addActionListener(okListener);
161                 _descriptionField.addActionListener(okListener);
162                 buttonPanel.add(okButton);
163                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
164                 cancelButton.addActionListener(new ActionListener() {
165                         public void actionPerformed(ActionEvent e)
166                         {
167                                 _dialog.dispose();
168                         }
169                 });
170                 buttonPanel.add(cancelButton);
171                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
172                 return dialogPanel;
173         }
174
175
176         /**
177          * Enable the checkboxes according to data
178          */
179         private void enableCheckboxes()
180         {
181                 boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
182                 if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
183                 boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
184                 _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
185                 _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
186         }
187
188
189         /**
190          * Start the export process based on the input parameters
191          */
192         private void startExport()
193         {
194                 // OK pressed, so choose output file
195                 if (_fileChooser == null)
196                 {
197                         _fileChooser = new JFileChooser();
198                         _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
199                         _fileChooser.setFileFilter(new GenericFileFilter("filetype.kmlkmz", new String[] {"kml", "kmz"}));
200                         // start from directory in config which should be set
201                         File configDir = Config.getWorkingDirectory();
202                         if (configDir != null) {_fileChooser.setCurrentDirectory(configDir);}
203                 }
204                 String requiredExtension = null, otherExtension = null;
205                 if (_kmzCheckbox.isSelected())
206                 {
207                         requiredExtension = ".kmz"; otherExtension = ".kml";
208                 }
209                 else
210                 {
211                         requiredExtension = ".kml"; otherExtension = ".kmz";
212                 }
213                 _fileChooser.setAcceptAllFileFilterUsed(false);
214                 // Allow choose again if an existing file is selected
215                 boolean chooseAgain = false;
216                 do
217                 {
218                         chooseAgain = false;
219                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
220                         {
221                                 // OK pressed and file chosen
222                                 File file = _fileChooser.getSelectedFile();
223                                 if (file.getName().toLowerCase().endsWith(otherExtension))
224                                 {
225                                         String path = file.getAbsolutePath();
226                                         file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
227                                 }
228                                 else if (!file.getName().toLowerCase().endsWith(requiredExtension))
229                                 {
230                                         file = new File(file.getAbsolutePath() + requiredExtension);
231                                 }
232                                 // Check if file exists and if necessary prompt for overwrite
233                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
234                                 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
235                                                 I18nManager.getText("dialog.save.overwrite.text"),
236                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
237                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
238                                         == JOptionPane.YES_OPTION)
239                                 {
240                                         // New file or overwrite confirmed, so initiate export in separate thread
241                                         _exportFile = file;
242                                         new Thread(this).start();
243                                 }
244                                 else
245                                 {
246                                         chooseAgain = true;
247                                 }
248                         }
249                 } while (chooseAgain);
250         }
251
252
253         /**
254          * Run method for controlling separate thread for exporting
255          */
256         public void run()
257         {
258                 // Initialise progress bar
259                 _progressBar.setVisible(true);
260                 _progressBar.setValue(0);
261                 boolean exportToKmz = _kmzCheckbox.isSelected();
262                 boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
263                 _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
264                 OutputStreamWriter writer = null;
265                 ZipOutputStream zipOutputStream = null;
266                 try
267                 {
268                         // Select writer according to whether kmz requested or not
269                         if (!_kmzCheckbox.isSelected())
270                         {
271                                 // normal writing to file
272                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
273                         }
274                         else
275                         {
276                                 // kmz requested - need zip output stream
277                                 zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
278                                 writer = new OutputStreamWriter(zipOutputStream);
279                                 // Make an entry in the zip file for the kml file
280                                 ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
281                                 zipOutputStream.putNextEntry(kmlEntry);
282                         }
283                         // write file
284                         int numPoints = exportData(writer, exportImages);
285                         // update progress bar
286                         _progressBar.setValue(1);
287
288                         // close zip entry if necessary
289                         if (zipOutputStream != null)
290                         {
291                                 // Make sure all buffered data in writer is flushed
292                                 writer.flush();
293                                 // Close off this entry in the zip file
294                                 zipOutputStream.closeEntry();
295                                 // Export images into zip file too if requested
296                                 if (exportImages)
297                                 {
298                                         // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
299                                         exportThumbnails(zipOutputStream);
300                                 }
301                         }
302
303                         // close file
304                         writer.close();
305                         // Store directory in config for later
306                         Config.setWorkingDirectory(_exportFile.getParentFile());
307                         // show confirmation
308                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
309                                  + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
310                                  + " " + _exportFile.getAbsolutePath());
311                         // export successful so need to close dialog and return
312                         _dialog.dispose();
313                         return;
314                 }
315                 catch (IOException ioe)
316                 {
317                         // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
318                         try {
319                                 if (writer != null) writer.close();
320                         }
321                         catch (IOException ioe2) {}
322                         JOptionPane.showMessageDialog(_parentFrame,
323                                 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
324                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
325                 }
326                 // if not returned already, export failed so need to recall the file selection
327                 startExport();
328         }
329
330
331         /**
332          * Export the information to the given writer
333          * @param inWriter writer object
334          * @param inExportImages true if image thumbnails are to be referenced
335          * @return number of points written
336          */
337         private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
338         throws IOException
339         {
340                 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
341                 inWriter.write("\t<name>");
342                 if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
343                 {
344                         inWriter.write(_descriptionField.getText());
345                 }
346                 else
347                 {
348                         inWriter.write("Export from Prune");
349                 }
350                 inWriter.write("</name>\n");
351
352                 boolean exportAltitudes = _altitudesCheckbox.isSelected();
353                 int i = 0;
354                 DataPoint point = null;
355                 boolean hasTrackpoints = false;
356                 // Loop over waypoints
357                 boolean writtenPhotoHeader = false;
358                 int numPoints = _track.getNumPoints();
359                 int photoNum = 0;
360                 for (i=0; i<numPoints; i++)
361                 {
362                         point = _track.getPoint(i);
363                         // Make a blob for each waypoint
364                         if (point.isWaypoint())
365                         {
366                                 exportWaypoint(point, inWriter, exportAltitudes);
367                         }
368                         else if (point.getPhoto() == null)
369                         {
370                                 hasTrackpoints = true;
371                         }
372                         // Make a blob with description for each photo
373                         if (point.getPhoto() != null)
374                         {
375                                 if (!writtenPhotoHeader)
376                                 {
377                                         inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
378                                         writtenPhotoHeader = true;
379                                 }
380                                 photoNum++;
381                                 exportPhotoPoint(point, inWriter, inExportImages, photoNum, exportAltitudes);
382                         }
383                 }
384                 // Make a line for the track, if there is one
385                 if (hasTrackpoints)
386                 {
387                         // Set up strings for start and end of track segment
388                         String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
389                                 + "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
390                                 + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
391                                 + "\t\t</Style>\n\t\t<LineString>\n";
392                         if (exportAltitudes) {
393                                 trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
394                         }
395                         trackStart += "\t\t\t<coordinates>";
396                         String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
397
398                         // Start segment
399                         inWriter.write(trackStart);
400                         // Loop over track points
401                         boolean firstTrackpoint = true;
402                         for (i=0; i<numPoints; i++)
403                         {
404                                 point = _track.getPoint(i);
405                                 // start new track segment if necessary
406                                 if (point.getSegmentStart() && !firstTrackpoint) {
407                                         inWriter.write(trackEnd);
408                                         inWriter.write(trackStart);
409                                 }
410                                 if (!point.isWaypoint() && point.getPhoto() == null)
411                                 {
412                                         exportTrackpoint(point, inWriter, exportAltitudes);
413                                         firstTrackpoint = false;
414                                 }
415                         }
416                         // end segment
417                         inWriter.write(trackEnd);
418                 }
419                 inWriter.write("</Folder>\n</kml>");
420                 return numPoints;
421         }
422
423
424         /**
425          * Export the specified waypoint into the file
426          * @param inPoint waypoint to export
427          * @param inWriter writer object
428          * @param inExportAltitude true to include altitude
429          * @throws IOException on write failure
430          */
431         private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
432         {
433                 inWriter.write("\t<Placemark>\n\t\t<name>");
434                 inWriter.write(inPoint.getWaypointName().trim());
435                 inWriter.write("</name>\n");
436                 inWriter.write("\t\t<Point>\n");
437                 if (inExportAltitude && inPoint.hasAltitude()) {
438                         inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
439                 }
440                 inWriter.write("\t\t\t<coordinates>");
441                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
442                 inWriter.write(',');
443                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
444                 inWriter.write(",");
445                 if (inExportAltitude && inPoint.hasAltitude()) {
446                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
447                 }
448                 else {
449                         inWriter.write("0");
450                 }
451                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
452         }
453
454
455         /**
456          * Export the specified photo into the file
457          * @param inPoint data point including photo
458          * @param inWriter writer object
459          * @param inImageLink flag to set whether to export image links or not
460          * @param inImageNumber number of image for filename
461          * @param inExportAltitude true to include altitude
462          * @throws IOException on write failure
463          */
464         private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
465                 int inImageNumber, boolean inExportAltitude)
466         throws IOException
467         {
468                 inWriter.write("\t<Placemark>\n\t\t<name>");
469                 inWriter.write(inPoint.getPhoto().getFile().getName());
470                 inWriter.write("</name>\n");
471                 if (inImageLink)
472                 {
473                         // Work out image dimensions of thumbnail
474                         Dimension picSize = inPoint.getPhoto().getSize();
475                         Dimension thumbSize = ImageUtils.getThumbnailSize(picSize.width, picSize.height, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
476                         // Write out some html for the thumbnail images
477                         inWriter.write("<description><![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
478                                 + inImageNumber + ".jpg' width='" + thumbSize.width + "' height='" + thumbSize.height + "'></center></td></tr>"
479                                 + "<tr><td><center>Caption for the photo</center></td></tr></table>]]></description>");
480                 }
481                 inWriter.write("<styleUrl>#camera_icon</styleUrl>\n");
482                 inWriter.write("\t\t<Point>\n");
483                 if (inExportAltitude && inPoint.hasAltitude()) {
484                         inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
485                 }
486                 inWriter.write("\t\t\t<coordinates>");
487                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
488                 inWriter.write(',');
489                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
490                 inWriter.write(",");
491                 if (inExportAltitude && inPoint.hasAltitude()) {
492                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
493                 }
494                 else {
495                         inWriter.write("0");
496                 }
497                 inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
498         }
499
500
501         /**
502          * Export the specified trackpoint into the file
503          * @param inPoint trackpoint to export
504          * @param inWriter writer object
505          * @param inExportAltitude true to include altitude
506          */
507         private void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
508         {
509                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
510                 inWriter.write(',');
511                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
512                 // Altitude either absolute or locked to ground by Google Earth
513                 inWriter.write(",");
514                 if (inExportAltitude && inPoint.hasAltitude()) {
515                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
516                 }
517                 else {
518                         inWriter.write("0");
519                 }
520                 inWriter.write("\n");
521         }
522
523
524         /**
525          * Loop through the photos and create thumbnails
526          * @param inZipStream zip stream to save image files to
527          */
528         private void exportThumbnails(ZipOutputStream inZipStream) throws IOException
529         {
530                 // set up image writer
531                 Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
532                 if (writers == null || !writers.hasNext())
533                 {
534                         throw new IOException("no JPEG writer found");
535                 }
536                 ImageWriter imageWriter = writers.next();
537
538                 int numPoints = _track.getNumPoints();
539                 DataPoint point = null;
540                 int photoNum = 0;
541                 // Loop over all points in track
542                 for (int i=0; i<numPoints; i++)
543                 {
544                         point = _track.getPoint(i);
545                         if (point.getPhoto() != null)
546                         {
547                                 photoNum++;
548                                 // Make a new entry in zip file
549                                 ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
550                                 inZipStream.putNextEntry(entry);
551                                 // Load image and write to outstream
552                                 ImageIcon icon = new ImageIcon(point.getPhoto().getFile().getAbsolutePath());
553
554                                 // Scale and smooth image to required size
555                                 Dimension outputSize = ImageUtils.getThumbnailSize(
556                                         point.getPhoto().getWidth(), point.getPhoto().getHeight(),
557                                         THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
558                                 BufferedImage bufferedImage = ImageUtils.createScaledImage(icon.getImage(), outputSize.width, outputSize.height);
559
560                                 imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
561                                 imageWriter.write(bufferedImage);
562                                 // Close zip file entry
563                                 inZipStream.closeEntry();
564                                 // Update progress bar
565                                 _progressBar.setValue(photoNum+1);
566                         }
567                 }
568         }
569
570
571         /**
572          * @return number of correlated photos in the track
573          */
574         private int getNumPhotosToExport()
575         {
576                 int numPoints = _track.getNumPoints();
577                 int numPhotos = 0;
578                 DataPoint point = null;
579                 // Loop over all points in track
580                 for (int i=0; i<numPoints; i++)
581                 {
582                         point = _track.getPoint(i);
583                         if (point.getPhoto() != null)
584                         {
585                                 numPhotos++;
586                         }
587                 }
588                 return numPhotos;
589         }
590 }