]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/GpxExporter.java
d35c35a8696be7e9863fca94fcb6c4c91dcf49ad
[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                 final String gpxHeader = getGpxHeaderString(gpxCachers);
347                 final boolean isVersion1_1 = (gpxHeader.toUpperCase().indexOf("GPX/1/1") > 0);
348                 inWriter.write(gpxHeader);
349                 // Name field
350                 String trackName = (inName != null && !inName.equals("")) ? inName : "GpsPruneTrack";
351                 writeNameAndDescription(inWriter, inName, inDesc, isVersion1_1);
352
353                 int i = 0;
354                 DataPoint point = null;
355                 final boolean exportTrackpoints = inSaveFlags[0];
356                 final boolean exportWaypoints = inSaveFlags[1];
357                 final boolean exportPhotos = inSaveFlags[2];
358                 final boolean exportAudios = inSaveFlags[3];
359                 final boolean exportSelection = inSaveFlags[4];
360                 final boolean exportTimestamps = inSaveFlags[5];
361                 // Examine selection
362                 int selStart = -1, selEnd = -1;
363                 if (exportSelection) {
364                         selStart = inInfo.getSelection().getStart();
365                         selEnd = inInfo.getSelection().getEnd();
366                 }
367                 // Loop over waypoints
368                 final int numPoints = inInfo.getTrack().getNumPoints();
369                 int numSaved = 0;
370                 for (i=0; i<numPoints; i++)
371                 {
372                         point = inInfo.getTrack().getPoint(i);
373                         if (!exportSelection || (i>=selStart && i<=selEnd))
374                         {
375                                 // Make a wpt element for each waypoint
376                                 if (point.isWaypoint() && exportWaypoints)
377                                 {
378                                         String pointSource = (inUseCopy?getPointSource(gpxCachers, point):null);
379                                         if (pointSource != null) {
380                                                 inWriter.write(pointSource);
381                                                 inWriter.write('\n');
382                                         }
383                                         else {
384                                                 exportWaypoint(point, inWriter, exportTimestamps, exportPhotos, exportAudios);
385                                         }
386                                         numSaved++;
387                                 }
388                         }
389                 }
390                 // Export both route points and then track points
391                 if (exportTrackpoints || exportPhotos || exportAudios)
392                 {
393                         // Output all route points (if any)
394                         numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
395                                 exportAudios, exportTimestamps, true, gpxCachers, "<rtept", "\t<rte><number>1</number>\n",
396                                 null, "\t</rte>\n");
397                         // Output all track points, if any
398                         String trackStart = "\t<trk><name>" + trackName + "</name><number>1</number><trkseg>\n";
399                         numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
400                                 exportAudios, exportTimestamps, false, gpxCachers, "<trkpt", trackStart,
401                                 "\t</trkseg>\n\t<trkseg>\n", "\t</trkseg></trk>\n");
402                 }
403
404                 inWriter.write("</gpx>\n");
405                 return numSaved;
406         }
407
408
409         /**
410          * Write the name and description according to the GPX version number
411          * @param inWriter writer object
412          * @param inName name, or null if none supplied
413          * @param inDesc description, or null if none supplied
414          * @param inIsVersion1_1 true if gpx version 1.1, false for version 1.0
415          */
416         private static void writeNameAndDescription(OutputStreamWriter inWriter,
417                 String inName, String inDesc, boolean inIsVersion1_1) throws IOException
418         {
419                 String desc = (inDesc != null && !inDesc.equals("")) ? inDesc : "Export from GpsPrune";
420                 // Position of name and description fields needs to be different for GPX1.0 and GPX1.1
421                 if (inIsVersion1_1)
422                 {
423                         // GPX 1.1 has the name and description inside a metadata tag
424                         inWriter.write("\t<metadata>\n");
425                 }
426                 if (inName != null && !inName.equals(""))
427                 {
428                         inWriter.write("\t\t<name>");
429                         inWriter.write(inName);
430                         inWriter.write("</name>\n");
431                 }
432                 inWriter.write("\t\t<desc>");
433                 inWriter.write(desc);
434                 inWriter.write("</desc>\n");
435                 if (inIsVersion1_1)
436                 {
437                         inWriter.write("\t</metadata>\n");
438                 }
439         }
440
441         /**
442          * Loop through the track outputting the relevant track points
443          * @param inWriter writer object for output
444          * @param inInfo track info object containing track
445          * @param inExportSelection true to just output current selection
446          * @param inExportTrackpoints true to output track points
447          * @param inExportPhotos true to output photo points
448          * @param inExportAudios true to output audio points
449          * @param inExportTimestamps true to include timestamps in export
450          * @param inOnlyCopies true to only export if source can be copied
451          * @param inCachers list of GpxCachers
452          * @param inPointTag tag to match for each point
453          * @param inStartTag start tag to output
454          * @param inSegmentTag tag to output between segments (or null)
455          * @param inEndTag end tag to output
456          */
457         private static int writeTrackPoints(OutputStreamWriter inWriter,
458                 TrackInfo inInfo, boolean inExportSelection, boolean inExportTrackpoints,
459                 boolean inExportPhotos, boolean inExportAudios, boolean exportTimestamps,
460                 boolean inOnlyCopies, GpxCacherList inCachers, String inPointTag,
461                 String inStartTag, String inSegmentTag, String inEndTag)
462         throws IOException
463         {
464                 // Note: far too many input parameters to this method but avoids duplication
465                 // of output functionality for writing track points and route points
466                 int numPoints = inInfo.getTrack().getNumPoints();
467                 int selStart = inInfo.getSelection().getStart();
468                 int selEnd = inInfo.getSelection().getEnd();
469                 int numSaved = 0;
470                 // Loop over track points
471                 for (int i=0; i<numPoints; i++)
472                 {
473                         DataPoint point = inInfo.getTrack().getPoint(i);
474                         if ((!inExportSelection || (i>=selStart && i<=selEnd)) && !point.isWaypoint())
475                         {
476                                 if ((point.getPhoto()==null && inExportTrackpoints) || (point.getPhoto()!=null && inExportPhotos)
477                                         || (point.getAudio()!=null && inExportAudios))
478                                 {
479                                         // get the source from the point (if any)
480                                         String pointSource = getPointSource(inCachers, point);
481                                         // Clear point source if it's the wrong type of point (eg changed from waypoint or route point)
482                                         if (pointSource != null && !pointSource.toLowerCase().startsWith(inPointTag)) {pointSource = null;}
483                                         if (pointSource != null || !inOnlyCopies)
484                                         {
485                                                 // restart track segment if necessary
486                                                 if ((numSaved > 0) && point.getSegmentStart() && (inSegmentTag != null)) {
487                                                         inWriter.write(inSegmentTag);
488                                                 }
489                                                 if (numSaved == 0) {inWriter.write(inStartTag);}
490                                                 if (pointSource != null) {
491                                                         inWriter.write(pointSource);
492                                                         inWriter.write('\n');
493                                                 }
494                                                 else {
495                                                         if (!inOnlyCopies) {exportTrackpoint(point, inWriter, exportTimestamps, inExportPhotos, inExportAudios);}
496                                                 }
497                                                 numSaved++;
498                                         }
499                                 }
500                         }
501                 }
502                 if (numSaved > 0) {inWriter.write(inEndTag);}
503                 return numSaved;
504         }
505
506
507         /**
508          * Get the point source for the specified point
509          * @param inCachers list of GPX cachers to ask for source
510          * @param inPoint point object
511          * @return xml source if available, or null otherwise
512          */
513         private static String getPointSource(GpxCacherList inCachers, DataPoint inPoint)
514         {
515                 if (inCachers == null || inPoint == null) {return null;}
516                 String source = inCachers.getSourceString(inPoint);
517                 if (source == null || !inPoint.isModified()) {return source;}
518                 // Point has been modified - maybe it's possible to modify the source
519                 source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
520                 source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
521                 source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
522                 source = replaceGpxTags(source, "<time>", "</time>", inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
523                 if (inPoint.isWaypoint())
524                 {
525                         source = replaceGpxTags(source, "<name>", "</name>", inPoint.getWaypointName());
526                         source = replaceGpxTags(source, "<description>", "</description>",
527                                 XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION)));
528                 }
529                 // photo / audio links
530                 if (source != null && (inPoint.hasMedia() || source.indexOf("</link>") > 0)) {
531                         source = replaceMediaLinks(source, makeMediaLink(inPoint));
532                 }
533                 return source;
534         }
535
536         /**
537          * Replace the given value into the given XML string
538          * @param inSource source XML for point
539          * @param inStartTag start tag for field
540          * @param inEndTag end tag for field
541          * @param inValue value to replace between start tag and end tag
542          * @return modified String, or null if not possible
543          */
544         private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
545         {
546                 if (inSource == null) {return null;}
547                 // Look for start and end tags within source
548                 final int startPos = inSource.indexOf(inStartTag);
549                 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
550                 if (startPos > 0 && endPos > 0)
551                 {
552                         String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
553                         if (inValue != null && origValue.equals(inValue)) {
554                                 // Value unchanged
555                                 return inSource;
556                         }
557                         else if (inValue == null || inValue.equals("")) {
558                                 // Need to delete value
559                                 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
560                         }
561                         else {
562                                 // Need to replace value
563                                 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
564                         }
565                 }
566                 // Value not found for this field in original source
567                 if (inValue == null || inValue.equals("")) {return inSource;}
568                 return null;
569         }
570
571
572         /**
573          * Replace the media tags in the given XML string
574          * @param inSource source XML for point
575          * @param inValue value for the current point
576          * @return modified String, or null if not possible
577          */
578         private static String replaceMediaLinks(String inSource, String inValue)
579         {
580                 if (inSource == null) {return null;}
581                 // Note that this method is very similar to replaceGpxTags except there can be multiple link tags
582                 // and the tags must have attributes.  So either one heavily parameterized method or two.
583                 // Look for start and end tags within source
584                 final String STARTTEXT = "<link";
585                 final String ENDTEXT = "</link>";
586                 final int startPos = inSource.indexOf(STARTTEXT);
587                 final int endPos = inSource.lastIndexOf(ENDTEXT);
588                 if (startPos > 0 && endPos > 0)
589                 {
590                         String origValue = inSource.substring(startPos, endPos + ENDTEXT.length());
591                         if (inValue != null && origValue.equals(inValue)) {
592                                 // Value unchanged
593                                 return inSource;
594                         }
595                         else if (inValue == null || inValue.equals("")) {
596                                 // Need to delete value
597                                 return inSource.substring(0, startPos) + inSource.substring(endPos + ENDTEXT.length());
598                         }
599                         else {
600                                 // Need to replace value
601                                 return inSource.substring(0, startPos) + inValue + inSource.substring(endPos + ENDTEXT.length());
602                         }
603                 }
604                 // Value not found for this field in original source
605                 if (inValue == null || inValue.equals("")) {return inSource;}
606                 return null;
607         }
608
609
610         /**
611          * Get the header string for the xml document including encoding
612          * @param inWriter writer object
613          * @return header string defining encoding
614          */
615         private static String getXmlHeaderString(OutputStreamWriter inWriter)
616         {
617                 return "<?xml version=\"1.0\" encoding=\"" + getEncoding(inWriter) + "\"?>\n";
618         }
619
620
621         /**
622          * Get the default system encoding using a writer
623          * @param inWriter writer object
624          * @return string defining encoding
625          */
626         private static String getEncoding(OutputStreamWriter inWriter)
627         {
628                 String encoding = inWriter.getEncoding();
629                 try {
630                         encoding =  Charset.forName(encoding).name();
631                 }
632                 catch (Exception e) {} // ignore failure to find encoding
633                 // Hack to fix bugs with Mac OSX (which reports MacRoman but is actually UTF-8)
634                 if (encoding == null || encoding.toLowerCase().startsWith("macroman")) {
635                         encoding = "UTF-8";
636                 }
637                 return encoding;
638         }
639
640
641         /**
642          * Use a temporary file to obtain the name of the default system encoding
643          * @return name of default system encoding, or null if write failed
644          */
645         private static String getSystemEncoding()
646         {
647                 File tempFile = null;
648                 String encoding = null;
649                 try
650                 {
651                         tempFile = File.createTempFile("prune", null);
652                         OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(tempFile));
653                         encoding = getEncoding(writer);
654                         writer.close();
655                 }
656                 catch (IOException e) {} // value stays null
657                 // Delete temp file
658                 if (tempFile != null && tempFile.exists()) {
659                         if (!tempFile.delete()) {
660                                 System.err.println("Cannot delete temp file: " + tempFile.getAbsolutePath());
661                         }
662                 }
663                 // If writing failed (eg permissions) then just ask system for default
664                 if (encoding == null) encoding = Charset.defaultCharset().name();
665                 return encoding;
666         }
667
668         /**
669          * Creates temp file if necessary to check system encoding
670          * @return true if system uses UTF-8 by default
671          */
672         private static boolean isSystemUtf8()
673         {
674                 if (_systemEncoding == null) _systemEncoding = getSystemEncoding();
675                 return (_systemEncoding != null && _systemEncoding.toUpperCase().equals("UTF-8"));
676         }
677
678         /**
679          * Get the header string for the gpx tag
680          * @param inCachers cacher list to ask for headers, if available
681          * @return header string from cachers or as default
682          */
683         private static String getGpxHeaderString(GpxCacherList inCachers)
684         {
685                 String gpxHeader = null;
686                 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
687                 if (gpxHeader == null || gpxHeader.length() < 5)
688                 {
689                         // Create default (1.0) header
690                         gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
691                                 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
692                                 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
693                                 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
694                 }
695                 return gpxHeader + "\n";
696         }
697
698
699         /**
700          * Export the specified waypoint into the file
701          * @param inPoint waypoint to export
702          * @param inWriter writer object
703          * @param inTimestamps true to export timestamps too
704          * @param inPhoto true to export link to photo
705          * @param inAudio true to export link to audio
706          * @throws IOException on write failure
707          */
708         private static void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps,
709                 boolean inPhoto, boolean inAudio)
710                 throws IOException
711         {
712                 inWriter.write("\t<wpt lat=\"");
713                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
714                 inWriter.write("\" lon=\"");
715                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
716                 inWriter.write("\">\n");
717                 // altitude if available
718                 if (inPoint.hasAltitude())
719                 {
720                         inWriter.write("\t\t<ele>");
721                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
722                         inWriter.write("</ele>\n");
723                 }
724                 // timestamp if available (point might have timestamp and then be turned into a waypoint)
725                 if (inPoint.hasTimestamp() && inTimestamps)
726                 {
727                         inWriter.write("\t\t<time>");
728                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
729                         inWriter.write("</time>\n");
730                 }
731                 // write waypoint name after elevation and time
732                 inWriter.write("\t\t<name>");
733                 inWriter.write(inPoint.getWaypointName().trim());
734                 inWriter.write("</name>\n");
735                 // description, if any
736                 String desc = XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION));
737                 if (desc != null && !desc.equals(""))
738                 {
739                         inWriter.write("\t\t<description>");
740                         inWriter.write(desc);
741                         inWriter.write("</description>\n");
742                 }
743                 // Media links, if any
744                 if (inPhoto && inPoint.getPhoto() != null)
745                 {
746                         inWriter.write("\t\t");
747                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
748                         inWriter.write('\n');
749                 }
750                 if (inAudio && inPoint.getAudio() != null)
751                 {
752                         inWriter.write("\t\t");
753                         inWriter.write(makeMediaLink(inPoint.getAudio()));
754                         inWriter.write('\n');
755                 }
756                 // write waypoint type if any
757                 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
758                 if (type != null)
759                 {
760                         type = type.trim();
761                         if (!type.equals(""))
762                         {
763                                 inWriter.write("\t\t<type>");
764                                 inWriter.write(type);
765                                 inWriter.write("</type>\n");
766                         }
767                 }
768                 inWriter.write("\t</wpt>\n");
769         }
770
771
772         /**
773          * Export the specified trackpoint into the file
774          * @param inPoint trackpoint to export
775          * @param inWriter writer object
776          * @param inTimestamps true to export timestamps too
777          * @param inExportPhoto true to export photo link
778          * @param inExportAudio true to export audio link
779          */
780         private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps,
781                 boolean inExportPhoto, boolean inExportAudio)
782                 throws IOException
783         {
784                 inWriter.write("\t\t<trkpt lat=\"");
785                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
786                 inWriter.write("\" lon=\"");
787                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
788                 inWriter.write("\">");
789                 // altitude
790                 if (inPoint.hasAltitude())
791                 {
792                         inWriter.write("<ele>");
793                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
794                         inWriter.write("</ele>");
795                 }
796                 // timestamp if available (and selected)
797                 if (inPoint.hasTimestamp() && inTimestamps)
798                 {
799                         inWriter.write("<time>");
800                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
801                         inWriter.write("</time>");
802                 }
803                 // photo, audio
804                 if (inPoint.getPhoto() != null && inExportPhoto) {
805                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
806                 }
807                 if (inPoint.getAudio() != null && inExportAudio) {
808                         inWriter.write(makeMediaLink(inPoint.getAudio()));
809                 }
810                 inWriter.write("</trkpt>\n");
811         }
812
813
814         /**
815          * Make the xml for the media link(s)
816          * @param inPoint point to generate text for
817          * @return link tags, or null if no links
818          */
819         private static String makeMediaLink(DataPoint inPoint)
820         {
821                 Photo photo = inPoint.getPhoto();
822                 AudioClip audio = inPoint.getAudio();
823                 if (photo == null && audio == null) {
824                         return null;
825                 }
826                 String linkText = "";
827                 if (photo != null) {
828                         linkText = makeMediaLink(photo);
829                 }
830                 if (audio != null) {
831                         linkText += makeMediaLink(audio);
832                 }
833                 return linkText;
834         }
835
836         /**
837          * Make the media link for a single media item
838          * @param inMedia media item, either photo or audio
839          * @return link for this media
840          */
841         private static String makeMediaLink(MediaObject inMedia)
842         {
843                 if (inMedia.getFile() != null)
844                         // file link
845                         return "<link href=\"" + inMedia.getFile().getAbsolutePath() + "\"><text>" + inMedia.getName() + "</text></link>";
846                 if (inMedia.getUrl() != null)
847                         // url link
848                         return "<link href=\"" + inMedia.getUrl() + "\"><text>" + inMedia.getName() + "</text></link>";
849                 // No link available, must have been loaded from zip file - no link possible
850                 return "";
851         }
852 }