]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/GpxExporter.java
0f038b77ce60f473a50a64b068c68b698f5b2050
[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 field
351                 String trackName = (inName != null && !inName.equals("")) ? inName : "GpsPruneTrack";
352                 writeNameAndDescription(inWriter, inName, inDesc, isVersion1_1);
353
354                 int i = 0;
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 (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                 String desc = (inDesc != null && !inDesc.equals("")) ? inDesc : "Export from GpsPrune";
427                 // Position of name and description fields needs to be different for GPX1.0 and GPX1.1
428                 if (inIsVersion1_1)
429                 {
430                         // GPX 1.1 has the name and description inside a metadata tag
431                         inWriter.write("\t<metadata>\n");
432                 }
433                 if (inName != null && !inName.equals(""))
434                 {
435                         if (inIsVersion1_1) {inWriter.write('\t');}
436                         inWriter.write("\t<name>");
437                         inWriter.write(inName);
438                         inWriter.write("</name>\n");
439                 }
440                 if (inIsVersion1_1) {inWriter.write('\t');}
441                 inWriter.write("\t<desc>");
442                 inWriter.write(desc);
443                 inWriter.write("</desc>\n");
444                 if (inIsVersion1_1)
445                 {
446                         inWriter.write("\t</metadata>\n");
447                 }
448         }
449
450         /**
451          * Loop through the track outputting the relevant track points
452          * @param inWriter writer object for output
453          * @param inInfo track info object containing track
454          * @param inExportSelection true to just output current selection
455          * @param inExportTrackpoints true to output track points
456          * @param inExportPhotos true to output photo points
457          * @param inExportAudios true to output audio points
458          * @param inExportTimestamps true to include timestamps in export
459          * @param inOnlyCopies true to only export if source can be copied
460          * @param inCachers list of GpxCachers
461          * @param inPointTag tag to match for each point
462          * @param inStartTag start tag to output
463          * @param inSegmentTag tag to output between segments (or null)
464          * @param inEndTag end tag to output
465          */
466         private static int writeTrackPoints(OutputStreamWriter inWriter,
467                 TrackInfo inInfo, boolean inExportSelection, boolean inExportTrackpoints,
468                 boolean inExportPhotos, boolean inExportAudios, boolean exportTimestamps,
469                 boolean inOnlyCopies, GpxCacherList inCachers, String inPointTag,
470                 String inStartTag, String inSegmentTag, String inEndTag)
471         throws IOException
472         {
473                 // Note: far too many input parameters to this method but avoids duplication
474                 // of output functionality for writing track points and route points
475                 int numPoints = inInfo.getTrack().getNumPoints();
476                 int selStart = inInfo.getSelection().getStart();
477                 int selEnd = inInfo.getSelection().getEnd();
478                 int numSaved = 0;
479                 // Loop over track points
480                 for (int i=0; i<numPoints; i++)
481                 {
482                         DataPoint point = inInfo.getTrack().getPoint(i);
483                         if ((!inExportSelection || (i>=selStart && i<=selEnd)) && !point.isWaypoint())
484                         {
485                                 if ((point.getPhoto()==null && inExportTrackpoints) || (point.getPhoto()!=null && inExportPhotos)
486                                         || (point.getAudio()!=null && inExportAudios))
487                                 {
488                                         // get the source from the point (if any)
489                                         String pointSource = getPointSource(inCachers, point);
490                                         // Clear point source if it's the wrong type of point (eg changed from waypoint or route point)
491                                         if (pointSource != null && !pointSource.trim().toLowerCase().startsWith(inPointTag)) {
492                                                 pointSource = null;
493                                         }
494                                         if (pointSource != null || !inOnlyCopies)
495                                         {
496                                                 // restart track segment if necessary
497                                                 if ((numSaved > 0) && point.getSegmentStart() && (inSegmentTag != null)) {
498                                                         inWriter.write(inSegmentTag);
499                                                 }
500                                                 if (numSaved == 0) {inWriter.write(inStartTag);}
501                                                 if (pointSource != null)
502                                                 {
503                                                         // If timestamps checkbox is off, strip the time
504                                                         if (!exportTimestamps) {
505                                                                 pointSource = stripTime(pointSource);
506                                                         }
507                                                         inWriter.write(pointSource);
508                                                         inWriter.write('\n');
509                                                 }
510                                                 else {
511                                                         if (!inOnlyCopies) {exportTrackpoint(point, inWriter, exportTimestamps, inExportPhotos, inExportAudios);}
512                                                 }
513                                                 numSaved++;
514                                         }
515                                 }
516                         }
517                 }
518                 if (numSaved > 0) {inWriter.write(inEndTag);}
519                 return numSaved;
520         }
521
522
523         /**
524          * Get the point source for the specified point
525          * @param inCachers list of GPX cachers to ask for source
526          * @param inPoint point object
527          * @return xml source if available, or null otherwise
528          */
529         private static String getPointSource(GpxCacherList inCachers, DataPoint inPoint)
530         {
531                 if (inCachers == null || inPoint == null) {return null;}
532                 String source = inCachers.getSourceString(inPoint);
533                 if (source == null || !inPoint.isModified()) {return source;}
534                 // Point has been modified - maybe it's possible to modify the source
535                 source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
536                 source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
537                 source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
538                 source = replaceGpxTags(source, "<time>", "</time>", inPoint.getTimestamp().getText(Timestamp.Format.ISO8601));
539                 if (inPoint.isWaypoint())
540                 {
541                         source = replaceGpxTags(source, "<name>", "</name>", inPoint.getWaypointName());
542                         source = replaceGpxTags(source, "<description>", "</description>",
543                                 XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION)));
544                 }
545                 // photo / audio links
546                 if (source != null && (inPoint.hasMedia() || source.indexOf("</link>") > 0)) {
547                         source = replaceMediaLinks(source, makeMediaLink(inPoint));
548                 }
549                 return source;
550         }
551
552         /**
553          * Replace the given value into the given XML string
554          * @param inSource source XML for point
555          * @param inStartTag start tag for field
556          * @param inEndTag end tag for field
557          * @param inValue value to replace between start tag and end tag
558          * @return modified String, or null if not possible
559          */
560         private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
561         {
562                 if (inSource == null) {return null;}
563                 // Look for start and end tags within source
564                 final int startPos = inSource.indexOf(inStartTag);
565                 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
566                 if (startPos > 0 && endPos > 0)
567                 {
568                         String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
569                         if (inValue != null && origValue.equals(inValue)) {
570                                 // Value unchanged
571                                 return inSource;
572                         }
573                         else if (inValue == null || inValue.equals("")) {
574                                 // Need to delete value
575                                 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
576                         }
577                         else {
578                                 // Need to replace value
579                                 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
580                         }
581                 }
582                 // Value not found for this field in original source
583                 if (inValue == null || inValue.equals("")) {return inSource;}
584                 return null;
585         }
586
587
588         /**
589          * Replace the media tags in the given XML string
590          * @param inSource source XML for point
591          * @param inValue value for the current point
592          * @return modified String, or null if not possible
593          */
594         private static String replaceMediaLinks(String inSource, String inValue)
595         {
596                 if (inSource == null) {return null;}
597                 // Note that this method is very similar to replaceGpxTags except there can be multiple link tags
598                 // and the tags must have attributes.  So either one heavily parameterized method or two.
599                 // Look for start and end tags within source
600                 final String STARTTEXT = "<link";
601                 final String ENDTEXT = "</link>";
602                 final int startPos = inSource.indexOf(STARTTEXT);
603                 final int endPos = inSource.lastIndexOf(ENDTEXT);
604                 if (startPos > 0 && endPos > 0)
605                 {
606                         String origValue = inSource.substring(startPos, endPos + ENDTEXT.length());
607                         if (inValue != null && origValue.equals(inValue)) {
608                                 // Value unchanged
609                                 return inSource;
610                         }
611                         else if (inValue == null || inValue.equals("")) {
612                                 // Need to delete value
613                                 return inSource.substring(0, startPos) + inSource.substring(endPos + ENDTEXT.length());
614                         }
615                         else {
616                                 // Need to replace value
617                                 return inSource.substring(0, startPos) + inValue + inSource.substring(endPos + ENDTEXT.length());
618                         }
619                 }
620                 // Value not found for this field in original source
621                 if (inValue == null || inValue.equals("")) {return inSource;}
622                 return null;
623         }
624
625
626         /**
627          * Get the header string for the xml document including encoding
628          * @param inWriter writer object
629          * @return header string defining encoding
630          */
631         private static String getXmlHeaderString(OutputStreamWriter inWriter)
632         {
633                 return "<?xml version=\"1.0\" encoding=\"" + XmlUtils.getEncoding(inWriter) + "\"?>\n";
634         }
635
636
637         /**
638          * Get the header string for the gpx tag
639          * @param inCachers cacher list to ask for headers, if available
640          * @return header string from cachers or as default
641          */
642         private static String getGpxHeaderString(GpxCacherList inCachers)
643         {
644                 String gpxHeader = null;
645                 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
646                 if (gpxHeader == null || gpxHeader.length() < 5)
647                 {
648                         // TODO: Consider changing this to default to GPX 1.1
649                         // Create default (1.0) header
650                         gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
651                                 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
652                                 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
653                                 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
654                 }
655                 return gpxHeader + "\n";
656         }
657
658
659         /**
660          * Export the specified waypoint into the file
661          * @param inPoint waypoint to export
662          * @param inWriter writer object
663          * @param inTimestamps true to export timestamps too
664          * @param inPhoto true to export link to photo
665          * @param inAudio true to export link to audio
666          * @throws IOException on write failure
667          */
668         private static void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps,
669                 boolean inPhoto, boolean inAudio)
670                 throws IOException
671         {
672                 inWriter.write("\t<wpt lat=\"");
673                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
674                 inWriter.write("\" lon=\"");
675                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
676                 inWriter.write("\">\n");
677                 // altitude if available
678                 if (inPoint.hasAltitude())
679                 {
680                         inWriter.write("\t\t<ele>");
681                         inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
682                         inWriter.write("</ele>\n");
683                 }
684                 // timestamp if available (point might have timestamp and then be turned into a waypoint)
685                 if (inPoint.hasTimestamp() && inTimestamps)
686                 {
687                         inWriter.write("\t\t<time>");
688                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.Format.ISO8601));
689                         inWriter.write("</time>\n");
690                 }
691                 // write waypoint name after elevation and time
692                 inWriter.write("\t\t<name>");
693                 inWriter.write(inPoint.getWaypointName().trim());
694                 inWriter.write("</name>\n");
695                 // description, if any
696                 String desc = XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION));
697                 if (desc != null && !desc.equals(""))
698                 {
699                         inWriter.write("\t\t<description>");
700                         inWriter.write(desc);
701                         inWriter.write("</description>\n");
702                 }
703                 // Media links, if any
704                 if (inPhoto && inPoint.getPhoto() != null)
705                 {
706                         inWriter.write("\t\t");
707                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
708                         inWriter.write('\n');
709                 }
710                 if (inAudio && inPoint.getAudio() != null)
711                 {
712                         inWriter.write("\t\t");
713                         inWriter.write(makeMediaLink(inPoint.getAudio()));
714                         inWriter.write('\n');
715                 }
716                 // write waypoint type if any
717                 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
718                 if (type != null)
719                 {
720                         type = type.trim();
721                         if (!type.equals(""))
722                         {
723                                 inWriter.write("\t\t<type>");
724                                 inWriter.write(type);
725                                 inWriter.write("</type>\n");
726                         }
727                 }
728                 inWriter.write("\t</wpt>\n");
729         }
730
731
732         /**
733          * Export the specified trackpoint into the file
734          * @param inPoint trackpoint to export
735          * @param inWriter writer object
736          * @param inTimestamps true to export timestamps too
737          * @param inExportPhoto true to export photo link
738          * @param inExportAudio true to export audio link
739          */
740         private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps,
741                 boolean inExportPhoto, boolean inExportAudio)
742                 throws IOException
743         {
744                 inWriter.write("\t\t\t<trkpt lat=\"");
745                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
746                 inWriter.write("\" lon=\"");
747                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
748                 inWriter.write("\">\n");
749                 // altitude
750                 if (inPoint.hasAltitude())
751                 {
752                         inWriter.write("\t\t\t\t<ele>");
753                         inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
754                         inWriter.write("</ele>\n");
755                 }
756                 // timestamp if available (and selected)
757                 if (inPoint.hasTimestamp() && inTimestamps)
758                 {
759                         inWriter.write("\t\t\t\t<time>");
760                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.Format.ISO8601));
761                         inWriter.write("</time>\n");
762                 }
763                 // photo, audio
764                 if (inPoint.getPhoto() != null && inExportPhoto) {
765                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
766                 }
767                 if (inPoint.getAudio() != null && inExportAudio) {
768                         inWriter.write(makeMediaLink(inPoint.getAudio()));
769                 }
770                 inWriter.write("\t\t\t</trkpt>\n");
771         }
772
773
774         /**
775          * Make the xml for the media link(s)
776          * @param inPoint point to generate text for
777          * @return link tags, or null if no links
778          */
779         private static String makeMediaLink(DataPoint inPoint)
780         {
781                 Photo photo = inPoint.getPhoto();
782                 AudioClip audio = inPoint.getAudio();
783                 if (photo == null && audio == null) {
784                         return null;
785                 }
786                 String linkText = "";
787                 if (photo != null) {
788                         linkText = makeMediaLink(photo);
789                 }
790                 if (audio != null) {
791                         linkText += makeMediaLink(audio);
792                 }
793                 return linkText;
794         }
795
796         /**
797          * Make the media link for a single media item
798          * @param inMedia media item, either photo or audio
799          * @return link for this media
800          */
801         private static String makeMediaLink(MediaObject inMedia)
802         {
803                 if (inMedia.getFile() != null)
804                         // file link
805                         return "<link href=\"" + inMedia.getFile().getAbsolutePath() + "\"><text>" + inMedia.getName() + "</text></link>";
806                 if (inMedia.getUrl() != null)
807                         // url link
808                         return "<link href=\"" + inMedia.getUrl() + "\"><text>" + inMedia.getName() + "</text></link>";
809                 // No link available, must have been loaded from zip file - no link possible
810                 return "";
811         }
812
813
814         /**
815          * Strip the time from a GPX point source string
816          * @param inPointSource point source to copy
817          * @return point source with timestamp removed
818          */
819         private static String stripTime(String inPointSource)
820         {
821                 return inPointSource.replaceAll("[ \t]*<time>.*?</time>", "");
822         }
823 }