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