]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/GpxExporter.java
Version 13, August 2011
[GpsPrune.git] / tim / prune / save / GpxExporter.java
1 package tim.prune.save;
2
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.FlowLayout;
6 import java.awt.GridLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.io.File;
10 import java.io.FileOutputStream;
11 import java.io.IOException;
12 import java.io.OutputStreamWriter;
13 import java.io.Writer;
14 import java.nio.charset.Charset;
15
16 import javax.swing.BorderFactory;
17 import javax.swing.Box;
18 import javax.swing.BoxLayout;
19 import javax.swing.ButtonGroup;
20 import javax.swing.JButton;
21 import javax.swing.JCheckBox;
22 import javax.swing.JDialog;
23 import javax.swing.JFileChooser;
24 import javax.swing.JFrame;
25 import javax.swing.JLabel;
26 import javax.swing.JOptionPane;
27 import javax.swing.JPanel;
28 import javax.swing.JRadioButton;
29 import javax.swing.JTextField;
30 import javax.swing.border.EtchedBorder;
31
32 import tim.prune.App;
33 import tim.prune.GenericFunction;
34 import tim.prune.GpsPrune;
35 import tim.prune.I18nManager;
36 import tim.prune.UpdateMessageBroker;
37 import tim.prune.config.Config;
38 import tim.prune.data.Altitude;
39 import tim.prune.data.AudioClip;
40 import tim.prune.data.Coordinate;
41 import tim.prune.data.DataPoint;
42 import tim.prune.data.Field;
43 import tim.prune.data.MediaObject;
44 import tim.prune.data.Photo;
45 import tim.prune.data.RecentFile;
46 import tim.prune.data.Timestamp;
47 import tim.prune.data.TrackInfo;
48 import tim.prune.gui.DialogCloser;
49 import tim.prune.load.GenericFileFilter;
50 import tim.prune.save.xml.GpxCacherList;
51 import tim.prune.save.xml.XmlUtils;
52
53
54 /**
55  * Class to export track information
56  * into a specified Gpx file
57  */
58 public class GpxExporter extends GenericFunction implements Runnable
59 {
60         private TrackInfo _trackInfo = null;
61         private JDialog _dialog = null;
62         private JTextField _nameField = null;
63         private JTextField _descriptionField = null;
64         private PointTypeSelector _pointTypeSelector = null;
65         private JCheckBox _timestampsCheckbox = null;
66         private JCheckBox _copySourceCheckbox = null;
67         private JPanel _encodingsPanel = null;
68         private JRadioButton _useSystemRadio = null, _forceUtf8Radio = null;
69         private File _exportFile = null;
70         private static String _systemEncoding = null;
71
72         /** this program name */
73         private static final String GPX_CREATOR = "GpsPrune v" + GpsPrune.VERSION_NUMBER + " activityworkshop.net";
74
75
76         /**
77          * Constructor
78          * @param inApp app object
79          */
80         public GpxExporter(App inApp)
81         {
82                 super(inApp);
83                 _trackInfo = inApp.getTrackInfo();
84         }
85
86         /** Get name key */
87         public String getNameKey() {
88                 return "function.exportgpx";
89         }
90
91         /**
92          * Show the dialog to select options and export file
93          */
94         public void begin()
95         {
96                 // Make dialog window
97                 if (_dialog == null)
98                 {
99                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
100                         _dialog.setLocationRelativeTo(_parentFrame);
101                         _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
102                         _systemEncoding = getSystemEncoding();
103                         _dialog.getContentPane().add(makeDialogComponents());
104                         _dialog.pack();
105                 }
106                 _pointTypeSelector.init(_app.getTrackInfo());
107                 _encodingsPanel.setVisible(!isSystemUtf8());
108                 if (!isSystemUtf8()) {
109                         _useSystemRadio.setText(I18nManager.getText("dialog.exportgpx.encoding.system")
110                                 + " (" + (_systemEncoding == null ? "unknown" : _systemEncoding) + ")");
111                 }
112                 _dialog.setVisible(true);
113         }
114
115
116         /**
117          * Create dialog components
118          * @return Panel containing all gui elements in dialog
119          */
120         private Component makeDialogComponents()
121         {
122                 JPanel dialogPanel = new JPanel();
123                 dialogPanel.setLayout(new BorderLayout());
124                 JPanel mainPanel = new JPanel();
125                 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
126                 // Make a panel for the name/desc text boxes
127                 JPanel descPanel = new JPanel();
128                 descPanel.setLayout(new GridLayout(2, 2));
129                 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.name")));
130                 _nameField = new JTextField(10);
131                 descPanel.add(_nameField);
132                 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.desc")));
133                 _descriptionField = new JTextField(10);
134                 descPanel.add(_descriptionField);
135                 mainPanel.add(descPanel);
136                 mainPanel.add(Box.createVerticalStrut(5));
137                 // point type selection (track points, waypoints, photo points)
138                 _pointTypeSelector = new PointTypeSelector();
139                 mainPanel.add(_pointTypeSelector);
140                 // checkboxes for timestamps and copying
141                 JPanel checkPanel = new JPanel();
142                 _timestampsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.includetimestamps"));
143                 _timestampsCheckbox.setSelected(true);
144                 checkPanel.add(_timestampsCheckbox);
145                 _copySourceCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.copysource"));
146                 _copySourceCheckbox.setSelected(true);
147                 checkPanel.add(_copySourceCheckbox);
148                 mainPanel.add(checkPanel);
149                 // panel for selecting character encoding
150                 _encodingsPanel = new JPanel();
151                 if (!isSystemUtf8())
152                 {
153                         // only add this panel if system isn't utf8 (or can't be identified yet)
154                         _encodingsPanel.setBorder(BorderFactory.createCompoundBorder(
155                                 BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4)));
156                         _encodingsPanel.setLayout(new BorderLayout());
157                         _encodingsPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.encoding")), BorderLayout.NORTH);
158                         JPanel radioPanel = new JPanel();
159                         radioPanel.setLayout(new FlowLayout());
160                         ButtonGroup radioGroup = new ButtonGroup();
161                         _useSystemRadio = new JRadioButton(I18nManager.getText("dialog.exportgpx.encoding.system"));
162                         _forceUtf8Radio = new JRadioButton(I18nManager.getText("dialog.exportgpx.encoding.utf8"));
163                         radioGroup.add(_useSystemRadio);
164                         radioGroup.add(_forceUtf8Radio);
165                         radioPanel.add(_useSystemRadio);
166                         radioPanel.add(_forceUtf8Radio);
167                         _useSystemRadio.setSelected(true);
168                         _encodingsPanel.add(radioPanel, BorderLayout.CENTER);
169                         mainPanel.add(_encodingsPanel);
170                 }
171                 dialogPanel.add(mainPanel, BorderLayout.CENTER);
172
173                 // close dialog if escape pressed
174                 _nameField.addKeyListener(new DialogCloser(_dialog));
175                 // button panel at bottom
176                 JPanel buttonPanel = new JPanel();
177                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
178                 JButton okButton = new JButton(I18nManager.getText("button.ok"));
179                 ActionListener okListener = new ActionListener() {
180                         public void actionPerformed(ActionEvent e)
181                         {
182                                 startExport();
183                         }
184                 };
185                 okButton.addActionListener(okListener);
186                 _descriptionField.addActionListener(okListener);
187                 buttonPanel.add(okButton);
188                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
189                 cancelButton.addActionListener(new ActionListener() {
190                         public void actionPerformed(ActionEvent e) {
191                                 _dialog.dispose();
192                         }
193                 });
194                 buttonPanel.add(cancelButton);
195                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
196                 dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
197                 return dialogPanel;
198         }
199
200
201         /**
202          * Start the export process based on the input parameters
203          */
204         private void startExport()
205         {
206                 // OK pressed, so check selections
207                 if (!_pointTypeSelector.getAnythingSelected())
208                 {
209                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
210                                 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
211                         return;
212                 }
213                 // Choose output file
214                 File saveFile = chooseGpxFile(_parentFrame);
215                 if (saveFile != null)
216                 {
217                         // New file or overwrite confirmed, so initiate export in separate thread
218                         _exportFile = saveFile;
219                         new Thread(this).start();
220                 }
221         }
222
223
224         /**
225          * Select a GPX file to save to
226          * @param inParentFrame parent frame for file chooser dialog
227          * @return selected File, or null if selection cancelled
228          */
229         public static File chooseGpxFile(JFrame inParentFrame)
230         {
231                 File saveFile = null;
232                 JFileChooser fileChooser = new JFileChooser();
233                 fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
234                 fileChooser.setFileFilter(new GenericFileFilter("filetype.gpx", new String[] {"gpx"}));
235                 fileChooser.setAcceptAllFileFilterUsed(false);
236                 // start from directory in config which should be set
237                 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
238                 if (configDir != null) {fileChooser.setCurrentDirectory(new File(configDir));}
239
240                 // Allow choose again if an existing file is selected
241                 boolean chooseAgain = false;
242                 do
243                 {
244                         chooseAgain = false;
245                         if (fileChooser.showSaveDialog(inParentFrame) == JFileChooser.APPROVE_OPTION)
246                         {
247                                 // OK pressed and file chosen
248                                 File file = fileChooser.getSelectedFile();
249                                 // Check file extension
250                                 if (!file.getName().toLowerCase().endsWith(".gpx"))
251                                 {
252                                         file = new File(file.getAbsolutePath() + ".gpx");
253                                 }
254                                 // Check if file exists and if necessary prompt for overwrite
255                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
256                                 if (!file.exists() || JOptionPane.showOptionDialog(inParentFrame,
257                                                 I18nManager.getText("dialog.save.overwrite.text"),
258                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
259                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
260                                         == JOptionPane.YES_OPTION)
261                                 {
262                                         // new file or overwrite confirmed
263                                         saveFile = file;
264                                 }
265                                 else
266                                 {
267                                         // file exists and overwrite cancelled - select again
268                                         chooseAgain = true;
269                                 }
270                         }
271                 } while (chooseAgain);
272                 return saveFile;
273         }
274
275
276         /**
277          * Run method for controlling separate thread for exporting
278          */
279         public void run()
280         {
281                 OutputStreamWriter writer = null;
282                 try
283                 {
284                         // normal writing to file - firstly specify UTF8 encoding if requested
285                         if (_forceUtf8Radio != null && _forceUtf8Radio.isSelected())
286                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile), "UTF-8");
287                         else
288                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
289                         final boolean[] saveFlags = {_pointTypeSelector.getTrackpointsSelected(), _pointTypeSelector.getWaypointsSelected(),
290                                 _pointTypeSelector.getPhotopointsSelected(), _pointTypeSelector.getAudiopointsSelected(),
291                                 _pointTypeSelector.getJustSelection(), _timestampsCheckbox.isSelected()};
292                         // write file
293                         final int numPoints = exportData(writer, _trackInfo, _nameField.getText(),
294                                 _descriptionField.getText(), saveFlags, _copySourceCheckbox.isSelected());
295
296                         // close file
297                         writer.close();
298                         // Store directory in config for later
299                         Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
300                         // Add to recent file list
301                         Config.getRecentFileList().addFile(new RecentFile(_exportFile, true));
302                         // Show confirmation
303                         UpdateMessageBroker.informSubscribers();
304                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
305                                  + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
306                                  + " " + _exportFile.getAbsolutePath());
307                         // export successful so need to close dialog and return
308                         _dialog.dispose();
309                         return;
310                 }
311                 catch (IOException ioe)
312                 {
313                         // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
314                         try {
315                                 if (writer != null) writer.close();
316                         }
317                         catch (IOException ioe2) {}
318                         JOptionPane.showMessageDialog(_parentFrame,
319                                 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
320                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
321                 }
322                 // if not returned already, export failed so need to recall the file selection
323                 startExport();
324         }
325
326
327         /**
328          * Export the information to the given writer
329          * @param inWriter writer object
330          * @param inInfo track info object
331          * @param inName name of track (optional)
332          * @param inDesc description of track (optional)
333          * @param inSaveFlags array of booleans to export tracks, waypoints, photos, audios, selection, timestamps
334          * @param inUseCopy true to copy source if available
335          * @return number of points written
336          * @throws IOException if io errors occur on write
337          */
338         public static int exportData(OutputStreamWriter inWriter, TrackInfo inInfo, String inName,
339                 String inDesc, boolean[] inSaveFlags, boolean inUseCopy) throws IOException
340         {
341                 // Instantiate source file cachers in case we want to copy output
342                 GpxCacherList gpxCachers = null;
343                 if (inUseCopy) gpxCachers = new GpxCacherList(inInfo.getFileInfo());
344                 // Write or copy headers
345                 inWriter.write(getXmlHeaderString(inWriter));
346                 inWriter.write(getGpxHeaderString(gpxCachers));
347                 // Name field
348                 String trackName = "GpsPruneTrack";
349                 if (inName != null && !inName.equals(""))
350                 {
351                         trackName = inName;
352                         inWriter.write("\t<name>");
353                         inWriter.write(trackName);
354                         inWriter.write("</name>\n");
355                 }
356                 // Description field
357                 inWriter.write("\t<desc>");
358                 inWriter.write((inDesc != null && !inDesc.equals(""))?inDesc:"Export from GpsPrune");
359                 inWriter.write("</desc>\n");
360
361                 int i = 0;
362                 DataPoint point = null;
363                 final boolean exportTrackpoints = inSaveFlags[0];
364                 final boolean exportWaypoints = inSaveFlags[1];
365                 final boolean exportPhotos = inSaveFlags[2];
366                 final boolean exportAudios = inSaveFlags[3];
367                 final boolean exportSelection = inSaveFlags[4];
368                 final boolean exportTimestamps = inSaveFlags[5];
369                 // Examine selection
370                 int selStart = -1, selEnd = -1;
371                 if (exportSelection) {
372                         selStart = inInfo.getSelection().getStart();
373                         selEnd = inInfo.getSelection().getEnd();
374                 }
375                 // Loop over waypoints
376                 final int numPoints = inInfo.getTrack().getNumPoints();
377                 int numSaved = 0;
378                 for (i=0; i<numPoints; i++)
379                 {
380                         point = inInfo.getTrack().getPoint(i);
381                         if (!exportSelection || (i>=selStart && i<=selEnd))
382                         {
383                                 // Make a wpt element for each waypoint
384                                 if (point.isWaypoint() && exportWaypoints)
385                                 {
386                                         String pointSource = (inUseCopy?getPointSource(gpxCachers, point):null);
387                                         if (pointSource != null) {
388                                                 inWriter.write(pointSource);
389                                                 inWriter.write('\n');
390                                         }
391                                         else {
392                                                 exportWaypoint(point, inWriter, exportTimestamps, exportPhotos, exportAudios);
393                                         }
394                                         numSaved++;
395                                 }
396                         }
397                 }
398                 // Export both route points and then track points
399                 if (exportTrackpoints || exportPhotos || exportAudios)
400                 {
401                         // Output all route points (if any)
402                         numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
403                                 exportAudios, exportTimestamps, true, gpxCachers, "<rtept", "\t<rte><number>1</number>\n",
404                                 null, "\t</rte>\n");
405                         // Output all track points, if any
406                         String trackStart = "\t<trk><name>" + trackName + "</name><number>1</number><trkseg>\n";
407                         numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
408                                 exportAudios, exportTimestamps, false, gpxCachers, "<trkpt", trackStart,
409                                 "\t</trkseg>\n\t<trkseg>\n", "\t</trkseg></trk>\n");
410                 }
411
412                 inWriter.write("</gpx>\n");
413                 return numSaved;
414         }
415
416
417         /**
418          * Loop through the track outputting the relevant track points
419          * @param inWriter writer object for output
420          * @param inInfo track info object containing track
421          * @param inExportSelection true to just output current selection
422          * @param inExportTrackpoints true to output track points
423          * @param inExportPhotos true to output photo points
424          * @param inExportAudios true to output audio points
425          * @param inExportTimestamps true to include timestamps in export
426          * @param inOnlyCopies true to only export if source can be copied
427          * @param inCachers list of GpxCachers
428          * @param inPointTag tag to match for each point
429          * @param inStartTag start tag to output
430          * @param inSegmentTag tag to output between segments (or null)
431          * @param inEndTag end tag to output
432          */
433         private static int writeTrackPoints(OutputStreamWriter inWriter,
434                 TrackInfo inInfo, boolean inExportSelection, boolean inExportTrackpoints,
435                 boolean inExportPhotos, boolean inExportAudios, boolean exportTimestamps,
436                 boolean inOnlyCopies, GpxCacherList inCachers, String inPointTag,
437                 String inStartTag, String inSegmentTag, String inEndTag)
438         throws IOException
439         {
440                 // Note: far too many input parameters to this method but avoids duplication
441                 // of output functionality for writing track points and route points
442                 int numPoints = inInfo.getTrack().getNumPoints();
443                 int selStart = inInfo.getSelection().getStart();
444                 int selEnd = inInfo.getSelection().getEnd();
445                 int numSaved = 0;
446                 // Loop over track points
447                 for (int i=0; i<numPoints; i++)
448                 {
449                         DataPoint point = inInfo.getTrack().getPoint(i);
450                         if ((!inExportSelection || (i>=selStart && i<=selEnd)) && !point.isWaypoint())
451                         {
452                                 if ((point.getPhoto()==null && inExportTrackpoints) || (point.getPhoto()!=null && inExportPhotos)
453                                         || (point.getAudio()!=null && inExportAudios))
454                                 {
455                                         // get the source from the point (if any)
456                                         String pointSource = getPointSource(inCachers, point);
457                                         // Clear point source if it's the wrong type of point (eg changed from waypoint or route point)
458                                         if (pointSource != null && !pointSource.toLowerCase().startsWith(inPointTag)) {pointSource = null;}
459                                         if (pointSource != null || !inOnlyCopies)
460                                         {
461                                                 // restart track segment if necessary
462                                                 if ((numSaved > 0) && point.getSegmentStart() && (inSegmentTag != null)) {
463                                                         inWriter.write(inSegmentTag);
464                                                 }
465                                                 if (numSaved == 0) {inWriter.write(inStartTag);}
466                                                 if (pointSource != null) {
467                                                         inWriter.write(pointSource);
468                                                         inWriter.write('\n');
469                                                 }
470                                                 else {
471                                                         if (!inOnlyCopies) {exportTrackpoint(point, inWriter, exportTimestamps, inExportPhotos, inExportAudios);}
472                                                 }
473                                                 numSaved++;
474                                         }
475                                 }
476                         }
477                 }
478                 if (numSaved > 0) {inWriter.write(inEndTag);}
479                 return numSaved;
480         }
481
482
483         /**
484          * Get the point source for the specified point
485          * @param inCachers list of GPX cachers to ask for source
486          * @param inPoint point object
487          * @return xml source if available, or null otherwise
488          */
489         private static String getPointSource(GpxCacherList inCachers, DataPoint inPoint)
490         {
491                 if (inCachers == null || inPoint == null) {return null;}
492                 String source = inCachers.getSourceString(inPoint);
493                 if (source == null || !inPoint.isModified()) {return source;}
494                 // Point has been modified - maybe it's possible to modify the source
495                 source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
496                 source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
497                 source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
498                 source = replaceGpxTags(source, "<time>", "</time>", inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
499                 if (inPoint.isWaypoint())
500                 {
501                         source = replaceGpxTags(source, "<name>", "</name>", inPoint.getWaypointName());
502                         source = replaceGpxTags(source, "<description>", "</description>",
503                                 XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION)));
504                 }
505                 // photo / audio links
506                 if (source != null && (inPoint.hasMedia() || source.indexOf("</link>") > 0)) {
507                         source = replaceMediaLinks(source, makeMediaLink(inPoint));
508                 }
509                 return source;
510         }
511
512         /**
513          * Replace the given value into the given XML string
514          * @param inSource source XML for point
515          * @param inStartTag start tag for field
516          * @param inEndTag end tag for field
517          * @param inValue value to replace between start tag and end tag
518          * @return modified String, or null if not possible
519          */
520         private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
521         {
522                 if (inSource == null) {return null;}
523                 // Look for start and end tags within source
524                 final int startPos = inSource.indexOf(inStartTag);
525                 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
526                 if (startPos > 0 && endPos > 0)
527                 {
528                         String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
529                         if (inValue != null && origValue.equals(inValue)) {
530                                 // Value unchanged
531                                 return inSource;
532                         }
533                         else if (inValue == null || inValue.equals("")) {
534                                 // Need to delete value
535                                 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
536                         }
537                         else {
538                                 // Need to replace value
539                                 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
540                         }
541                 }
542                 // Value not found for this field in original source
543                 if (inValue == null || inValue.equals("")) {return inSource;}
544                 return null;
545         }
546
547
548         /**
549          * Replace the media tags in the given XML string
550          * @param inSource source XML for point
551          * @param inValue value for the current point
552          * @return modified String, or null if not possible
553          */
554         private static String replaceMediaLinks(String inSource, String inValue)
555         {
556                 if (inSource == null) {return null;}
557                 // Note that this method is very similar to replaceGpxTags except there can be multiple link tags
558                 // and the tags must have attributes.  So either one heavily parameterized method or two.
559                 // Look for start and end tags within source
560                 final String STARTTEXT = "<link";
561                 final String ENDTEXT = "</link>";
562                 final int startPos = inSource.indexOf(STARTTEXT);
563                 final int endPos = inSource.lastIndexOf(ENDTEXT);
564                 if (startPos > 0 && endPos > 0)
565                 {
566                         String origValue = inSource.substring(startPos, endPos + ENDTEXT.length());
567                         if (inValue != null && origValue.equals(inValue)) {
568                                 // Value unchanged
569                                 return inSource;
570                         }
571                         else if (inValue == null || inValue.equals("")) {
572                                 // Need to delete value
573                                 return inSource.substring(0, startPos) + inSource.substring(endPos + ENDTEXT.length());
574                         }
575                         else {
576                                 // Need to replace value
577                                 return inSource.substring(0, startPos) + inValue + inSource.substring(endPos + ENDTEXT.length());
578                         }
579                 }
580                 // Value not found for this field in original source
581                 if (inValue == null || inValue.equals("")) {return inSource;}
582                 return null;
583         }
584
585
586         /**
587          * Get the header string for the xml document including encoding
588          * @param inWriter writer object
589          * @return header string defining encoding
590          */
591         private static String getXmlHeaderString(OutputStreamWriter inWriter)
592         {
593                 return "<?xml version=\"1.0\" encoding=\"" + getEncoding(inWriter) + "\"?>\n";
594         }
595
596
597         /**
598          * Get the default system encoding using a writer
599          * @param inWriter writer object
600          * @return string defining encoding
601          */
602         private static String getEncoding(OutputStreamWriter inWriter)
603         {
604                 String encoding = inWriter.getEncoding();
605                 try {
606                         encoding =  Charset.forName(encoding).name();
607                 }
608                 catch (Exception e) {} // ignore failure to find encoding
609                 // Hack to fix bugs with Mac OSX (which reports MacRoman but is actually UTF-8)
610                 if (encoding == null || encoding.toLowerCase().startsWith("macroman")) {
611                         encoding = "UTF-8";
612                 }
613                 return encoding;
614         }
615
616
617         /**
618          * Use a temporary file to obtain the name of the default system encoding
619          * @return name of default system encoding, or null if write failed
620          */
621         private static String getSystemEncoding()
622         {
623                 File tempFile = null;
624                 String encoding = null;
625                 try
626                 {
627                         tempFile = File.createTempFile("prune", null);
628                         OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(tempFile));
629                         encoding = getEncoding(writer);
630                         writer.close();
631                 }
632                 catch (IOException e) {} // value stays null
633                 // Delete temp file
634                 if (tempFile != null && tempFile.exists()) {
635                         if (!tempFile.delete()) {
636                                 System.err.println("Cannot delete temp file: " + tempFile.getAbsolutePath());
637                         }
638                 }
639                 // If writing failed (eg permissions) then just ask system for default
640                 if (encoding == null) encoding = Charset.defaultCharset().name();
641                 return encoding;
642         }
643
644         /**
645          * Creates temp file if necessary to check system encoding
646          * @return true if system uses UTF-8 by default
647          */
648         private static boolean isSystemUtf8()
649         {
650                 if (_systemEncoding == null) _systemEncoding = getSystemEncoding();
651                 return (_systemEncoding != null && _systemEncoding.toUpperCase().equals("UTF-8"));
652         }
653
654         /**
655          * Get the header string for the gpx tag
656          * @param inCachers cacher list to ask for headers, if available
657          * @return header string from cachers or as default
658          */
659         private static String getGpxHeaderString(GpxCacherList inCachers)
660         {
661                 String gpxHeader = null;
662                 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
663                 if (gpxHeader == null || gpxHeader.length() < 5)
664                 {
665                         // Create default (1.0) header
666                         gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
667                                 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
668                                 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
669                                 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
670                 }
671                 return gpxHeader + "\n";
672         }
673
674
675         /**
676          * Export the specified waypoint into the file
677          * @param inPoint waypoint to export
678          * @param inWriter writer object
679          * @param inTimestamps true to export timestamps too
680          * @param inPhoto true to export link to photo
681          * @param inAudio true to export link to audio
682          * @throws IOException on write failure
683          */
684         private static void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps,
685                 boolean inPhoto, boolean inAudio)
686                 throws IOException
687         {
688                 inWriter.write("\t<wpt lat=\"");
689                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
690                 inWriter.write("\" lon=\"");
691                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
692                 inWriter.write("\">\n");
693                 // altitude if available
694                 if (inPoint.hasAltitude())
695                 {
696                         inWriter.write("\t\t<ele>");
697                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
698                         inWriter.write("</ele>\n");
699                 }
700                 // timestamp if available (point might have timestamp and then be turned into a waypoint)
701                 if (inPoint.hasTimestamp() && inTimestamps)
702                 {
703                         inWriter.write("\t\t<time>");
704                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
705                         inWriter.write("</time>\n");
706                 }
707                 // write waypoint name after elevation and time
708                 inWriter.write("\t\t<name>");
709                 inWriter.write(inPoint.getWaypointName().trim());
710                 inWriter.write("</name>\n");
711                 // description, if any
712                 String desc = XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION));
713                 if (desc != null && !desc.equals(""))
714                 {
715                         inWriter.write("\t\t<description>");
716                         inWriter.write(desc);
717                         inWriter.write("</description>\n");
718                 }
719                 // Media links, if any
720                 if (inPhoto && inPoint.getPhoto() != null)
721                 {
722                         inWriter.write("\t\t");
723                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
724                         inWriter.write('\n');
725                 }
726                 if (inAudio && inPoint.getAudio() != null)
727                 {
728                         inWriter.write("\t\t");
729                         inWriter.write(makeMediaLink(inPoint.getAudio()));
730                         inWriter.write('\n');
731                 }
732                 // write waypoint type if any
733                 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
734                 if (type != null)
735                 {
736                         type = type.trim();
737                         if (!type.equals(""))
738                         {
739                                 inWriter.write("\t\t<type>");
740                                 inWriter.write(type);
741                                 inWriter.write("</type>\n");
742                         }
743                 }
744                 inWriter.write("\t</wpt>\n");
745         }
746
747
748         /**
749          * Export the specified trackpoint into the file
750          * @param inPoint trackpoint to export
751          * @param inWriter writer object
752          * @param inTimestamps true to export timestamps too
753          * @param inExportPhoto true to export photo link
754          * @param inExportAudio true to export audio link
755          */
756         private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps,
757                 boolean inExportPhoto, boolean inExportAudio)
758                 throws IOException
759         {
760                 inWriter.write("\t\t<trkpt lat=\"");
761                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
762                 inWriter.write("\" lon=\"");
763                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
764                 inWriter.write("\">");
765                 // altitude
766                 if (inPoint.hasAltitude())
767                 {
768                         inWriter.write("<ele>");
769                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
770                         inWriter.write("</ele>");
771                 }
772                 // timestamp if available (and selected)
773                 if (inPoint.hasTimestamp() && inTimestamps)
774                 {
775                         inWriter.write("<time>");
776                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
777                         inWriter.write("</time>");
778                 }
779                 // photo, audio
780                 if (inPoint.getPhoto() != null && inExportPhoto) {
781                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
782                 }
783                 if (inPoint.getAudio() != null && inExportAudio) {
784                         inWriter.write(makeMediaLink(inPoint.getAudio()));
785                 }
786                 inWriter.write("</trkpt>\n");
787         }
788
789
790         /**
791          * Make the xml for the media link(s)
792          * @param inPoint point to generate text for
793          * @return link tags, or null if no links
794          */
795         private static String makeMediaLink(DataPoint inPoint)
796         {
797                 Photo photo = inPoint.getPhoto();
798                 AudioClip audio = inPoint.getAudio();
799                 if (photo == null && audio == null) {
800                         return null;
801                 }
802                 String linkText = "";
803                 if (photo != null) {
804                         linkText = makeMediaLink(photo);
805                 }
806                 if (audio != null) {
807                         linkText += makeMediaLink(audio);
808                 }
809                 return linkText;
810         }
811
812         /**
813          * Make the media link for a single media item
814          * @param inMedia media item, either photo or audio
815          * @return link for this media
816          */
817         private static String makeMediaLink(MediaObject inMedia)
818         {
819                 if (inMedia.getFile() != null)
820                         // file link
821                         return "<link href=\"" + inMedia.getFile().getAbsolutePath() + "\"><text>" + inMedia.getName() + "</text></link>";
822                 if (inMedia.getUrl() != null)
823                         // url link
824                         return "<link href=\"" + inMedia.getUrl() + "\"><text>" + inMedia.getName() + "</text></link>";
825                 // No link available, must have been loaded from zip file - no link possible
826                 return "";
827         }
828 }