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