]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/GpxExporter.java
Version 19.2, December 2018
[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.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                 }
587                 // photo / audio links
588                 if (source != null && (inPoint.hasMedia() || source.indexOf("</link>") > 0)) {
589                         source = replaceMediaLinks(source, makeMediaLink(inPoint));
590                 }
591                 return source;
592         }
593
594         /**
595          * Replace the given value into the given XML string
596          * @param inSource source XML for point
597          * @param inStartTag start tag for field
598          * @param inEndTag end tag for field
599          * @param inValue value to replace between start tag and end tag
600          * @return modified String, or null if not possible
601          */
602         private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
603         {
604                 if (inSource == null) {return null;}
605                 // Look for start and end tags within source
606                 final int startPos = inSource.indexOf(inStartTag);
607                 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
608                 if (startPos > 0 && endPos > 0)
609                 {
610                         String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
611                         if (inValue != null && origValue.equals(inValue)) {
612                                 // Value unchanged
613                                 return inSource;
614                         }
615                         else if (inValue == null || inValue.equals("")) {
616                                 // Need to delete value
617                                 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
618                         }
619                         else {
620                                 // Need to replace value
621                                 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
622                         }
623                 }
624                 // Value not found for this field in original source
625                 if (inValue == null || inValue.equals("")) {return inSource;}
626                 return null;
627         }
628
629
630         /**
631          * Replace the media tags in the given XML string
632          * @param inSource source XML for point
633          * @param inValue value for the current point
634          * @return modified String, or null if not possible
635          */
636         private static String replaceMediaLinks(String inSource, String inValue)
637         {
638                 if (inSource == null) {return null;}
639                 // Note that this method is very similar to replaceGpxTags except there can be multiple link tags
640                 // and the tags must have attributes.  So either one heavily parameterized method or two.
641                 // Look for start and end tags within source
642                 final String STARTTEXT = "<link";
643                 final String ENDTEXT = "</link>";
644                 final int startPos = inSource.indexOf(STARTTEXT);
645                 final int endPos = inSource.lastIndexOf(ENDTEXT);
646                 if (startPos > 0 && endPos > 0)
647                 {
648                         String origValue = inSource.substring(startPos, endPos + ENDTEXT.length());
649                         if (inValue != null && origValue.equals(inValue)) {
650                                 // Value unchanged
651                                 return inSource;
652                         }
653                         else if (inValue == null || inValue.equals("")) {
654                                 // Need to delete value
655                                 return inSource.substring(0, startPos) + inSource.substring(endPos + ENDTEXT.length());
656                         }
657                         else {
658                                 // Need to replace value
659                                 return inSource.substring(0, startPos) + inValue + inSource.substring(endPos + ENDTEXT.length());
660                         }
661                 }
662                 // Value not found for this field in original source
663                 if (inValue == null || inValue.equals("")) {return inSource;}
664                 return null;
665         }
666
667
668         /**
669          * Get the header string for the xml document including encoding
670          * @param inWriter writer object
671          * @return header string defining encoding
672          */
673         private static String getXmlHeaderString(OutputStreamWriter inWriter)
674         {
675                 return "<?xml version=\"1.0\" encoding=\"" + XmlUtils.getEncoding(inWriter) + "\"?>\n";
676         }
677
678
679         /**
680          * Get the header string for the gpx tag
681          * @param inCachers cacher list to ask for headers, if available
682          * @return header string from cachers or as default
683          */
684         private static String getGpxHeaderString(GpxCacherList inCachers)
685         {
686                 String gpxHeader = null;
687                 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
688                 if (gpxHeader == null || gpxHeader.length() < 5)
689                 {
690                         // TODO: Consider changing this to default to GPX 1.1
691                         // Create default (1.0) header
692                         gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
693                                 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
694                                 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
695                                 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
696                 }
697                 return gpxHeader + "\n";
698         }
699
700
701         /**
702          * Export the specified waypoint into the file
703          * @param inPoint waypoint to export
704          * @param inWriter writer object
705          * @param inSettings export settings
706          * @throws IOException on write failure
707          */
708         private static void exportWaypoint(DataPoint inPoint, Writer inWriter,
709                 SettingsForExport inSettings)
710                 throws IOException
711         {
712                 inWriter.write("\t<wpt lat=\"");
713                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
714                 inWriter.write("\" lon=\"");
715                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
716                 inWriter.write("\">\n");
717                 // altitude if available
718                 if (inPoint.hasAltitude() || inSettings.getExportMissingAltitudesAsZero())
719                 {
720                         inWriter.write("\t\t<ele>");
721                         inWriter.write(inPoint.hasAltitude() ? inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES) : "0");
722                         inWriter.write("</ele>\n");
723                 }
724                 // timestamp if available (some waypoints have timestamps, some not)
725                 if (inPoint.hasTimestamp() && inSettings.getExportTimestamps())
726                 {
727                         inWriter.write("\t\t<time>");
728                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.Format.ISO8601, null));
729                         inWriter.write("</time>\n");
730                 }
731                 // write waypoint name after elevation and time
732                 inWriter.write("\t\t<name>");
733                 inWriter.write(XmlUtils.fixCdata(inPoint.getWaypointName().trim()));
734                 inWriter.write("</name>\n");
735                 // description, if any
736                 String desc = XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION));
737                 if (desc != null && !desc.equals(""))
738                 {
739                         inWriter.write("\t\t<desc>");
740                         inWriter.write(desc);
741                         inWriter.write("</desc>\n");
742                 }
743                 // Media links, if any
744                 if (inSettings.getExportPhotoPoints() && inPoint.getPhoto() != null)
745                 {
746                         inWriter.write("\t\t");
747                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
748                         inWriter.write('\n');
749                 }
750                 if (inSettings.getExportAudioPoints() && inPoint.getAudio() != null)
751                 {
752                         inWriter.write("\t\t");
753                         inWriter.write(makeMediaLink(inPoint.getAudio()));
754                         inWriter.write('\n');
755                 }
756                 // write waypoint type if any
757                 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
758                 if (type != null)
759                 {
760                         type = type.trim();
761                         if (!type.equals(""))
762                         {
763                                 inWriter.write("\t\t<type>");
764                                 inWriter.write(type);
765                                 inWriter.write("</type>\n");
766                         }
767                 }
768                 inWriter.write("\t</wpt>\n");
769         }
770
771
772         /**
773          * Export the specified trackpoint into the file
774          * @param inPoint trackpoint to export
775          * @param inWriter writer object
776          * @param inSettings export settings
777          */
778         private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, SettingsForExport inSettings)
779                 throws IOException
780         {
781                 inWriter.write("\t\t\t<trkpt lat=\"");
782                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
783                 inWriter.write("\" lon=\"");
784                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
785                 inWriter.write("\">\n");
786                 // altitude
787                 if (inPoint.hasAltitude() || inSettings.getExportMissingAltitudesAsZero())
788                 {
789                         inWriter.write("\t\t\t\t<ele>");
790                         inWriter.write(inPoint.hasAltitude() ? inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES) : "0");
791                         inWriter.write("</ele>\n");
792                 }
793                 // Maybe take timestamp from photo if the point hasn't got one
794                 Timestamp pointTimestamp = getPointTimestamp(inPoint, inSettings);
795                 // timestamp if available (and selected)
796                 if (pointTimestamp != null && inSettings.getExportTimestamps())
797                 {
798                         inWriter.write("\t\t\t\t<time>");
799                         inWriter.write(pointTimestamp.getText(Timestamp.Format.ISO8601, null));
800                         inWriter.write("</time>\n");
801                 }
802                 // photo, audio
803                 if (inPoint.getPhoto() != null && inSettings.getExportPhotoPoints())
804                 {
805                         inWriter.write("\t\t\t\t");
806                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
807                         inWriter.write("\n");
808                 }
809                 if (inPoint.getAudio() != null && inSettings.getExportAudioPoints()) {
810                         inWriter.write(makeMediaLink(inPoint.getAudio()));
811                 }
812                 inWriter.write("\t\t\t</trkpt>\n");
813         }
814
815
816         /**
817          * Make the xml for the media link(s)
818          * @param inPoint point to generate text for
819          * @return link tags, or null if no links
820          */
821         private static String makeMediaLink(DataPoint inPoint)
822         {
823                 Photo photo = inPoint.getPhoto();
824                 AudioClip audio = inPoint.getAudio();
825                 if (photo == null && audio == null) {
826                         return null;
827                 }
828                 String linkText = "";
829                 if (photo != null) {
830                         linkText = makeMediaLink(photo);
831                 }
832                 if (audio != null) {
833                         linkText += makeMediaLink(audio);
834                 }
835                 return linkText;
836         }
837
838         /**
839          * Make the media link for a single media item
840          * @param inMedia media item, either photo or audio
841          * @return link for this media
842          */
843         private static String makeMediaLink(MediaObject inMedia)
844         {
845                 if (inMedia.getFile() != null)
846                         // file link
847                         return "<link href=\"" + inMedia.getFile().getAbsolutePath() + "\"><text>" + inMedia.getName() + "</text></link>";
848                 if (inMedia.getUrl() != null)
849                         // url link
850                         return "<link href=\"" + inMedia.getUrl() + "\"><text>" + inMedia.getName() + "</text></link>";
851                 // No link available, must have been loaded from zip file - no link possible
852                 return "";
853         }
854
855
856         /**
857          * Strip the time from a GPX point source string
858          * @param inPointSource point source to copy
859          * @return point source with timestamp removed
860          */
861         private static String stripTime(String inPointSource)
862         {
863                 return inPointSource.replaceAll("[ \t]*<time>.*?</time>", "");
864         }
865
866         /**
867          * Get the timestamp from the point or its media
868          * @param inPoint point object
869          * @param inSettings export settings
870          * @return Timestamp object if available, or null
871          */
872         private static Timestamp getPointTimestamp(DataPoint inPoint, SettingsForExport inSettings)
873         {
874                 if (inPoint.hasTimestamp())
875                 {
876                         return inPoint.getTimestamp();
877                 }
878                 if (inPoint.getPhoto() != null && inSettings.getExportPhotoPoints())
879                 {
880                         if (inPoint.getPhoto().hasTimestamp())
881                         {
882                                 return inPoint.getPhoto().getTimestamp();
883                         }
884                 }
885                 if (inPoint.getAudio() != null && inSettings.getExportAudioPoints())
886                 {
887                         if (inPoint.getAudio().hasTimestamp())
888                         {
889                                 return inPoint.getAudio().getTimestamp();
890                         }
891                 }
892                 return null;
893         }
894 }