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