001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io.importexport;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagLayout;
007import java.awt.event.ActionListener;
008import java.awt.event.KeyAdapter;
009import java.awt.event.KeyEvent;
010import java.io.File;
011import java.io.IOException;
012import java.io.OutputStream;
013import java.text.MessageFormat;
014import java.time.Year;
015import java.util.Optional;
016
017import javax.swing.JButton;
018import javax.swing.JCheckBox;
019import javax.swing.JLabel;
020import javax.swing.JList;
021import javax.swing.JOptionPane;
022import javax.swing.JPanel;
023import javax.swing.JScrollPane;
024import javax.swing.ListSelectionModel;
025
026import org.openstreetmap.josm.data.gpx.GpxConstants;
027import org.openstreetmap.josm.data.gpx.GpxData;
028import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
029import org.openstreetmap.josm.gui.ExtendedDialog;
030import org.openstreetmap.josm.gui.MainApplication;
031import org.openstreetmap.josm.gui.layer.GpxLayer;
032import org.openstreetmap.josm.gui.layer.Layer;
033import org.openstreetmap.josm.gui.layer.OsmDataLayer;
034import org.openstreetmap.josm.gui.widgets.JosmTextArea;
035import org.openstreetmap.josm.gui.widgets.JosmTextField;
036import org.openstreetmap.josm.io.Compression;
037import org.openstreetmap.josm.io.GpxWriter;
038import org.openstreetmap.josm.spi.preferences.Config;
039import org.openstreetmap.josm.tools.CheckParameterUtil;
040import org.openstreetmap.josm.tools.GBC;
041
042/**
043 * Exports data to a .gpx file. Data may be native GPX or OSM data which will be converted.
044 * @since 1949
045 */
046public class GpxExporter extends FileExporter implements GpxConstants {
047
048    private static final String GPL_WARNING = "<html><font color='red' size='-2'>"
049        + tr("Note: GPL is not compatible with the OSM license. Do not upload GPL licensed tracks.") + "</html>";
050
051    private static final String[] LICENSES = {
052            "Creative Commons By-SA",
053            "Open Database License (ODbL)",
054            "public domain",
055            "GNU Lesser Public License (LGPL)",
056            "BSD License (MIT/X11)"};
057
058    private static final String[] URLS = {
059            "https://creativecommons.org/licenses/by-sa/3.0",
060            "http://opendatacommons.org/licenses/odbl/1.0",
061            "public domain",
062            "https://www.gnu.org/copyleft/lesser.html",
063            "http://www.opensource.org/licenses/bsd-license.php"};
064
065    /**
066     * Constructs a new {@code GpxExporter}.
067     */
068    public GpxExporter() {
069        super(GpxImporter.getFileFilter());
070    }
071
072    @Override
073    public boolean acceptFile(File pathname, Layer layer) {
074        if (!(layer instanceof OsmDataLayer) && !(layer instanceof GpxLayer))
075            return false;
076        return super.acceptFile(pathname, layer);
077    }
078
079    @Override
080    public void exportData(File file, Layer layer) throws IOException {
081        exportData(file, layer, false);
082    }
083
084    @Override
085    public void exportDataQuiet(File file, Layer layer) throws IOException {
086        exportData(file, layer, true);
087    }
088
089    private void exportData(File file, Layer layer, boolean quiet) throws IOException {
090        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
091        if (!(layer instanceof OsmDataLayer) && !(layer instanceof GpxLayer))
092            throw new IllegalArgumentException(MessageFormat.format("Expected instance of OsmDataLayer or GpxLayer. Got ''{0}''.", layer
093                    .getClass().getName()));
094        CheckParameterUtil.ensureParameterNotNull(file, "file");
095
096        String fn = file.getPath();
097        if (fn.indexOf('.') == -1) {
098            fn += ".gpx";
099            file = new File(fn);
100        }
101
102        GpxData gpxData;
103        if (quiet) {
104            gpxData = getGpxData(layer, file);
105            try (OutputStream fo = Compression.getCompressedFileOutputStream(file)) {
106                GpxWriter w = new GpxWriter(fo);
107                w.write(gpxData);
108                w.close();
109                fo.flush();
110            }
111            return;
112        }
113
114        // open the dialog asking for options
115        JPanel p = new JPanel(new GridBagLayout());
116
117        // At this moment, we only need to know the attributes of the GpxData,
118        // conversion of OsmDataLayer (if needed) will be done after the dialog is closed.
119        if (layer instanceof GpxLayer) {
120            gpxData = ((GpxLayer) layer).data;
121        } else {
122            gpxData = new GpxData();
123        }
124
125        p.add(new JLabel(tr("GPS track description")), GBC.eol());
126        JosmTextArea desc = new JosmTextArea(3, 40);
127        desc.setWrapStyleWord(true);
128        desc.setLineWrap(true);
129        desc.setText(gpxData.getString(META_DESC));
130        p.add(new JScrollPane(desc), GBC.eop().fill(GBC.BOTH));
131
132        JCheckBox author = new JCheckBox(tr("Add author information"), Config.getPref().getBoolean("lastAddAuthor", true));
133        p.add(author, GBC.eol());
134
135        JLabel nameLabel = new JLabel(tr("Real name"));
136        p.add(nameLabel, GBC.std().insets(10, 0, 5, 0));
137        JosmTextField authorName = new JosmTextField();
138        p.add(authorName, GBC.eol().fill(GBC.HORIZONTAL));
139        nameLabel.setLabelFor(authorName);
140
141        JLabel emailLabel = new JLabel(tr("E-Mail"));
142        p.add(emailLabel, GBC.std().insets(10, 0, 5, 0));
143        JosmTextField email = new JosmTextField();
144        p.add(email, GBC.eol().fill(GBC.HORIZONTAL));
145        emailLabel.setLabelFor(email);
146
147        JLabel copyrightLabel = new JLabel(tr("Copyright (URL)"));
148        p.add(copyrightLabel, GBC.std().insets(10, 0, 5, 0));
149        JosmTextField copyright = new JosmTextField();
150        p.add(copyright, GBC.std().fill(GBC.HORIZONTAL));
151        copyrightLabel.setLabelFor(copyright);
152
153        JButton predefined = new JButton(tr("Predefined"));
154        p.add(predefined, GBC.eol().insets(5, 0, 0, 0));
155
156        JLabel copyrightYearLabel = new JLabel(tr("Copyright year"));
157        p.add(copyrightYearLabel, GBC.std().insets(10, 0, 5, 5));
158        JosmTextField copyrightYear = new JosmTextField("");
159        p.add(copyrightYear, GBC.eol().fill(GBC.HORIZONTAL));
160        copyrightYearLabel.setLabelFor(copyrightYear);
161
162        JLabel warning = new JLabel("<html><font size='-2'>&nbsp;</html");
163        p.add(warning, GBC.eol().fill(GBC.HORIZONTAL).insets(15, 0, 0, 0));
164        addDependencies(gpxData, author, authorName, email, copyright, predefined, copyrightYear, nameLabel, emailLabel,
165                copyrightLabel, copyrightYearLabel, warning);
166
167        p.add(new JLabel(tr("Keywords")), GBC.eol());
168        JosmTextField keywords = new JosmTextField();
169        keywords.setText(gpxData.getString(META_KEYWORDS));
170        p.add(keywords, GBC.eol().fill(GBC.HORIZONTAL));
171
172        boolean sel = Config.getPref().getBoolean("gpx.export.colors", true);
173        JCheckBox colors = new JCheckBox(tr("Save track colors in GPX file"), sel);
174        p.add(colors, GBC.eol().fill(GBC.HORIZONTAL));
175        JCheckBox garmin = new JCheckBox(tr("Use Garmin compatible GPX extensions"),
176                Config.getPref().getBoolean("gpx.export.colors.garmin", false));
177        garmin.setEnabled(sel);
178        p.add(garmin, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 0));
179
180        boolean hasPrefs = !gpxData.getLayerPrefs().isEmpty();
181        JCheckBox layerPrefs = new JCheckBox(tr("Save layer specific preferences"),
182                hasPrefs && Config.getPref().getBoolean("gpx.export.prefs", true));
183        layerPrefs.setEnabled(hasPrefs);
184        p.add(layerPrefs, GBC.eop().fill(GBC.HORIZONTAL));
185
186        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(),
187                tr("Export options"),
188                tr("Export and Save"), tr("Cancel"))
189            .setButtonIcons("exportgpx", "cancel")
190            .setContent(p);
191
192        colors.addActionListener(l -> {
193            garmin.setEnabled(colors.isSelected());
194        });
195
196        garmin.addActionListener(l -> {
197            if (garmin.isSelected() && !ConditionalOptionPaneUtil.showConfirmationDialog(
198                    "gpx_color_garmin",
199                    ed,
200                    new JLabel("<html>" + tr("Garmin track extensions only support 16 colors.") + "<br>"
201                                        + tr("If you continue, the closest supported track color will be used.")
202                             + "</html>"),
203                    tr("Information"),
204                    JOptionPane.OK_CANCEL_OPTION,
205                    JOptionPane.INFORMATION_MESSAGE,
206                    JOptionPane.OK_OPTION)) {
207                garmin.setSelected(false);
208            }
209        });
210
211        if (ed.showDialog().getValue() != 1) {
212            setCanceled(true);
213            return;
214        }
215        setCanceled(false);
216
217        Config.getPref().putBoolean("lastAddAuthor", author.isSelected());
218        if (!authorName.getText().isEmpty()) {
219            Config.getPref().put("lastAuthorName", authorName.getText());
220        }
221        if (!copyright.getText().isEmpty()) {
222            Config.getPref().put("lastCopyright", copyright.getText());
223        }
224        Config.getPref().putBoolean("gpx.export.colors", colors.isSelected());
225        Config.getPref().putBoolean("gpx.export.colors.garmin", garmin.isSelected());
226        if (hasPrefs) {
227            Config.getPref().putBoolean("gpx.export.prefs", layerPrefs.isSelected());
228        }
229        ColorFormat cFormat = null;
230        if (colors.isSelected()) {
231            cFormat = garmin.isSelected() ? ColorFormat.GPXX : ColorFormat.GPXD;
232        }
233
234        gpxData = getGpxData(layer, file);
235
236        // add author and copyright details to the gpx data
237        if (author.isSelected()) {
238            if (!authorName.getText().isEmpty()) {
239                gpxData.put(META_AUTHOR_NAME, authorName.getText());
240                gpxData.put(META_COPYRIGHT_AUTHOR, authorName.getText());
241            }
242            if (!email.getText().isEmpty()) {
243                gpxData.put(META_AUTHOR_EMAIL, email.getText());
244            }
245            if (!copyright.getText().isEmpty()) {
246                gpxData.put(META_COPYRIGHT_LICENSE, copyright.getText());
247            }
248            if (!copyrightYear.getText().isEmpty()) {
249                gpxData.put(META_COPYRIGHT_YEAR, copyrightYear.getText());
250            }
251        }
252
253        // add the description to the gpx data
254        if (!desc.getText().isEmpty()) {
255            gpxData.put(META_DESC, desc.getText());
256        }
257
258        // add keywords to the gpx data
259        if (!keywords.getText().isEmpty()) {
260            gpxData.put(META_KEYWORDS, keywords.getText());
261        }
262
263        try (OutputStream fo = Compression.getCompressedFileOutputStream(file)) {
264            GpxWriter w = new GpxWriter(fo);
265            w.write(gpxData, cFormat, layerPrefs.isSelected());
266            w.close();
267            fo.flush();
268        }
269    }
270
271    private static GpxData getGpxData(Layer layer, File file) {
272        if (layer instanceof OsmDataLayer) {
273            return ((OsmDataLayer) layer).toGpxData();
274        } else if (layer instanceof GpxLayer) {
275            return ((GpxLayer) layer).data;
276        }
277        return OsmDataLayer.toGpxData(MainApplication.getLayerManager().getEditDataSet(), file);
278    }
279
280    private static void enableCopyright(final GpxData data, final JosmTextField copyright, final JButton predefined,
281            final JosmTextField copyrightYear, final JLabel copyrightLabel, final JLabel copyrightYearLabel,
282            final JLabel warning, boolean enable) {
283        copyright.setEnabled(enable);
284        predefined.setEnabled(enable);
285        copyrightYear.setEnabled(enable);
286        copyrightLabel.setEnabled(enable);
287        copyrightYearLabel.setEnabled(enable);
288        warning.setText(enable ? GPL_WARNING : "<html><font size='-2'>&nbsp;</html");
289
290        if (enable) {
291            if (copyrightYear.getText().isEmpty()) {
292                copyrightYear.setText(Optional.ofNullable(data.getString(META_COPYRIGHT_YEAR)).orElseGet(
293                        () -> Year.now().toString()));
294            }
295            if (copyright.getText().isEmpty()) {
296                copyright.setText(Optional.ofNullable(data.getString(META_COPYRIGHT_LICENSE)).orElseGet(
297                        () -> Config.getPref().get("lastCopyright", "https://creativecommons.org/licenses/by-sa/2.5")));
298                copyright.setCaretPosition(0);
299            }
300        } else {
301            copyrightYear.setText("");
302            copyright.setText("");
303        }
304    }
305
306    // CHECKSTYLE.OFF: ParameterNumber
307
308    /**
309     * Add all those listeners to handle the enable state of the fields.
310     * @param data GPX data
311     * @param author Author checkbox
312     * @param authorName Author name textfield
313     * @param email E-mail textfield
314     * @param copyright Copyright textfield
315     * @param predefined Predefined button
316     * @param copyrightYear Copyright year textfield
317     * @param nameLabel Name label
318     * @param emailLabel E-mail label
319     * @param copyrightLabel Copyright label
320     * @param copyrightYearLabel Copyright year label
321     * @param warning Warning label
322     */
323    private static void addDependencies(
324            final GpxData data,
325            final JCheckBox author,
326            final JosmTextField authorName,
327            final JosmTextField email,
328            final JosmTextField copyright,
329            final JButton predefined,
330            final JosmTextField copyrightYear,
331            final JLabel nameLabel,
332            final JLabel emailLabel,
333            final JLabel copyrightLabel,
334            final JLabel copyrightYearLabel,
335            final JLabel warning) {
336
337        // CHECKSTYLE.ON: ParameterNumber
338        ActionListener authorActionListener = e -> {
339            boolean b = author.isSelected();
340            authorName.setEnabled(b);
341            email.setEnabled(b);
342            nameLabel.setEnabled(b);
343            emailLabel.setEnabled(b);
344            if (b) {
345                authorName.setText(Optional.ofNullable(data.getString(META_AUTHOR_NAME)).orElseGet(
346                        () -> Config.getPref().get("lastAuthorName")));
347                email.setText(Optional.ofNullable(data.getString(META_AUTHOR_EMAIL)).orElseGet(
348                        () -> Config.getPref().get("lastAuthorEmail")));
349            } else {
350                authorName.setText("");
351                email.setText("");
352            }
353            boolean isAuthorSet = !authorName.getText().isEmpty();
354            GpxExporter.enableCopyright(data, copyright, predefined, copyrightYear, copyrightLabel, copyrightYearLabel, warning,
355                    b && isAuthorSet);
356        };
357        author.addActionListener(authorActionListener);
358
359        KeyAdapter authorNameListener = new KeyAdapter() {
360            @Override public void keyReleased(KeyEvent e) {
361                boolean b = !authorName.getText().isEmpty() && author.isSelected();
362                GpxExporter.enableCopyright(data, copyright, predefined, copyrightYear, copyrightLabel, copyrightYearLabel, warning, b);
363            }
364        };
365        authorName.addKeyListener(authorNameListener);
366
367        predefined.addActionListener(e -> {
368            JList<String> l = new JList<>(LICENSES);
369            l.setVisibleRowCount(LICENSES.length);
370            l.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
371            int answer = JOptionPane.showConfirmDialog(
372                    MainApplication.getMainFrame(),
373                    new JScrollPane(l),
374                    tr("Choose a predefined license"),
375                    JOptionPane.OK_CANCEL_OPTION,
376                    JOptionPane.QUESTION_MESSAGE
377            );
378            if (answer != JOptionPane.OK_OPTION || l.getSelectedIndex() == -1)
379                return;
380            StringBuilder license = new StringBuilder();
381            for (int i : l.getSelectedIndices()) {
382                if (i == 2) {
383                    license = new StringBuilder("public domain");
384                    break;
385                }
386                if (license.length() > 0) {
387                    license.append(", ");
388                }
389                license.append(URLS[i]);
390            }
391            copyright.setText(license.toString());
392            copyright.setCaretPosition(0);
393        });
394
395        authorActionListener.actionPerformed(null);
396        authorNameListener.keyReleased(null);
397    }
398}