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