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