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