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