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.io.File;
007import java.io.IOException;
008import java.io.InputStream;
009
010import javax.swing.JOptionPane;
011
012import org.openstreetmap.josm.actions.ExtensionFileFilter;
013import org.openstreetmap.josm.data.gpx.GpxData;
014import org.openstreetmap.josm.gui.MainApplication;
015import org.openstreetmap.josm.gui.Notification;
016import org.openstreetmap.josm.gui.layer.GpxLayer;
017import org.openstreetmap.josm.gui.layer.ImageryLayer;
018import org.openstreetmap.josm.gui.layer.OsmDataLayer;
019import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
020import org.openstreetmap.josm.gui.progress.ProgressMonitor;
021import org.openstreetmap.josm.gui.util.GuiHelper;
022import org.openstreetmap.josm.io.Compression;
023import org.openstreetmap.josm.io.GpxReader;
024import org.openstreetmap.josm.spi.preferences.Config;
025import org.openstreetmap.josm.tools.Logging;
026import org.xml.sax.SAXException;
027
028/**
029 * File importer allowing to import GPX files (*.gpx/gpx.gz files).
030 *
031 */
032public class GpxImporter extends FileImporter {
033
034    /**
035     * Utility class containing imported GPX and marker layers, and a task to run after they are added to MapView.
036     */
037    public static class GpxImporterData {
038        /**
039         * The imported GPX layer. May be null if no GPX data.
040         */
041        private final GpxLayer gpxLayer;
042        /**
043         * The imported marker layer. May be null if no marker.
044         */
045        private final MarkerLayer markerLayer;
046        /**
047         * The task to run after GPX and/or marker layer has been added to MapView.
048         */
049        private final Runnable postLayerTask;
050
051        /**
052         * Constructs a new {@code GpxImporterData}.
053         * @param gpxLayer The imported GPX layer. May be null if no GPX data.
054         * @param markerLayer The imported marker layer. May be null if no marker.
055         * @param postLayerTask The task to run after GPX and/or marker layer has been added to MapView.
056         */
057        public GpxImporterData(GpxLayer gpxLayer, MarkerLayer markerLayer, Runnable postLayerTask) {
058            this.gpxLayer = gpxLayer;
059            this.markerLayer = markerLayer;
060            this.postLayerTask = postLayerTask;
061        }
062
063        /**
064         * Returns the imported GPX layer. May be null if no GPX data.
065         * @return the imported GPX layer. May be null if no GPX data.
066         */
067        public GpxLayer getGpxLayer() {
068            return gpxLayer;
069        }
070
071        /**
072         * Returns the imported marker layer. May be null if no marker.
073         * @return the imported marker layer. May be null if no marker.
074         */
075        public MarkerLayer getMarkerLayer() {
076            return markerLayer;
077        }
078
079        /**
080         * Returns the task to run after GPX and/or marker layer has been added to MapView.
081         * @return the task to run after GPX and/or marker layer has been added to MapView.
082         */
083        public Runnable getPostLayerTask() {
084            return postLayerTask;
085        }
086    }
087
088    /**
089     * Constructs a new {@code GpxImporter}.
090     */
091    public GpxImporter() {
092        super(getFileFilter());
093    }
094
095    /**
096     * Returns a GPX file filter (*.gpx and *.gpx.gz files).
097     * @return a GPX file filter
098     */
099    public static ExtensionFileFilter getFileFilter() {
100        return ExtensionFileFilter.newFilterWithArchiveExtensions("gpx",
101                Config.getPref().get("save.extension.gpx", "gpx"), tr("GPX Files"), true);
102    }
103
104    @Override
105    public void importData(File file, ProgressMonitor progressMonitor) throws IOException {
106        final String fileName = file.getName();
107
108        try (InputStream is = Compression.getUncompressedFileInputStream(file)) {
109            GpxReader r = new GpxReader(is);
110            boolean parsedProperly = r.parse(true);
111            r.getGpxData().storageFile = file;
112            addLayers(loadLayers(r.getGpxData(), parsedProperly, fileName, tr("Markers from {0}", fileName)));
113        } catch (SAXException e) {
114            Logging.error(e);
115            throw new IOException(e.getLocalizedMessage(), e);
116        }
117    }
118
119    /**
120     * Adds the specified GPX and marker layers to Map.main
121     * @param data The layers to add
122     * @see #loadLayers
123     */
124    public static void addLayers(final GpxImporterData data) {
125        // FIXME: remove UI stuff from the IO subsystem
126        GuiHelper.runInEDT(() -> {
127            if (data.markerLayer != null) {
128                MainApplication.getLayerManager().addLayer(data.markerLayer);
129            }
130            if (data.gpxLayer != null) {
131                MainApplication.getLayerManager().addLayer(data.gpxLayer);
132            }
133            data.postLayerTask.run();
134        });
135    }
136
137    /**
138     * Replies the new GPX and marker layers corresponding to the specified GPX data.
139     * @param data The GPX data
140     * @param parsedProperly True if GPX data has been properly parsed by {@link GpxReader#parse}
141     * @param gpxLayerName The GPX layer name
142     * @param markerLayerName The marker layer name
143     * @return the new GPX and marker layers corresponding to the specified GPX data, to be used with {@link #addLayers}
144     * @see #addLayers
145     */
146    public static GpxImporterData loadLayers(final GpxData data, final boolean parsedProperly,
147            final String gpxLayerName, String markerLayerName) {
148        GpxLayer gpxLayer = null;
149        MarkerLayer markerLayer = null;
150        gpxLayer = new GpxLayer(data, gpxLayerName, data.storageFile != null);
151        if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !data.waypoints.isEmpty()) {
152            markerLayer = new MarkerLayer(data, markerLayerName, data.storageFile, gpxLayer);
153            if (markerLayer.data.isEmpty()) {
154                markerLayer = null;
155            } else {
156                gpxLayer.setLinkedMarkerLayer(markerLayer);
157            }
158        }
159
160        final boolean isSameColor = MainApplication.getLayerManager()
161                .getLayersOfType(ImageryLayer.class)
162                .stream().noneMatch(ImageryLayer::isVisible)
163                && data.getTracks().stream().anyMatch(t -> OsmDataLayer.getBackgroundColor().equals(t.getColor()));
164
165        Runnable postLayerTask = () -> {
166            if (!parsedProperly) {
167                String msg;
168                if (data.storageFile == null) {
169                    msg = tr("Error occurred while parsing gpx data for layer ''{0}''. Only a part of the file will be available.",
170                            gpxLayerName);
171                } else {
172                    msg = tr("Error occurred while parsing gpx file ''{0}''. Only a part of the file will be available.",
173                            data.storageFile.getPath());
174                }
175                JOptionPane.showMessageDialog(null, msg);
176            }
177            if (isSameColor) {
178                new Notification(tr("The imported track \"{0}\" might not be visible because it has the same color as the background." +
179                        "<br>You can change this in the context menu of the imported layer.", gpxLayerName))
180                .setIcon(JOptionPane.WARNING_MESSAGE)
181                .setDuration(Notification.TIME_LONG)
182                .show();
183            }
184        };
185        return new GpxImporterData(gpxLayer, markerLayer, postLayerTask);
186    }
187
188    /**
189     * Replies the new GPX and marker layers corresponding to the specified GPX file.
190     * @param is input stream to GPX data
191     * @param associatedFile GPX file
192     * @param gpxLayerName The GPX layer name
193     * @param markerLayerName The marker layer name
194     * @param progressMonitor The progress monitor
195     * @return the new GPX and marker layers corresponding to the specified GPX file
196     * @throws IOException if an I/O error occurs
197     */
198    public static GpxImporterData loadLayers(InputStream is, final File associatedFile,
199            final String gpxLayerName, String markerLayerName, ProgressMonitor progressMonitor) throws IOException {
200        try {
201            final GpxReader r = new GpxReader(is);
202            final boolean parsedProperly = r.parse(true);
203            r.getGpxData().storageFile = associatedFile;
204            return loadLayers(r.getGpxData(), parsedProperly, gpxLayerName, markerLayerName);
205        } catch (SAXException e) {
206            Logging.error(e);
207            throw new IOException(tr("Parsing data for layer ''{0}'' failed", gpxLayerName), e);
208        }
209    }
210}