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