001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.Color;
008import java.awt.Dimension;
009import java.awt.Graphics2D;
010import java.io.File;
011import java.text.DateFormat;
012import java.util.ArrayList;
013import java.util.Arrays;
014import java.util.Collection;
015import java.util.Date;
016import java.util.LinkedList;
017import java.util.List;
018
019import javax.swing.Action;
020import javax.swing.Icon;
021import javax.swing.JScrollPane;
022import javax.swing.SwingUtilities;
023
024import org.openstreetmap.josm.Main;
025import org.openstreetmap.josm.actions.RenameLayerAction;
026import org.openstreetmap.josm.actions.SaveActionBase;
027import org.openstreetmap.josm.data.Bounds;
028import org.openstreetmap.josm.data.SystemOfMeasurement;
029import org.openstreetmap.josm.data.gpx.GpxConstants;
030import org.openstreetmap.josm.data.gpx.GpxData;
031import org.openstreetmap.josm.data.gpx.GpxTrack;
032import org.openstreetmap.josm.data.gpx.WayPoint;
033import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
034import org.openstreetmap.josm.data.projection.Projection;
035import org.openstreetmap.josm.gui.MapView;
036import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
037import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
038import org.openstreetmap.josm.gui.layer.gpx.ChooseTrackVisibilityAction;
039import org.openstreetmap.josm.gui.layer.gpx.ConvertToDataLayerAction;
040import org.openstreetmap.josm.gui.layer.gpx.CustomizeDrawingAction;
041import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongTrackAction;
042import org.openstreetmap.josm.gui.layer.gpx.DownloadWmsAlongTrackAction;
043import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper;
044import org.openstreetmap.josm.gui.layer.gpx.ImportAudioAction;
045import org.openstreetmap.josm.gui.layer.gpx.ImportImagesAction;
046import org.openstreetmap.josm.gui.layer.gpx.MarkersFromNamedPointsAction;
047import org.openstreetmap.josm.gui.widgets.HtmlPanel;
048import org.openstreetmap.josm.io.GpxImporter;
049import org.openstreetmap.josm.tools.ImageProvider;
050import org.openstreetmap.josm.tools.date.DateUtils;
051
052public class GpxLayer extends Layer {
053
054    /** GPX data */
055    public GpxData data;
056    private final boolean isLocalFile;
057    // used by ChooseTrackVisibilityAction to determine which tracks to show/hide
058    public boolean[] trackVisibility = new boolean[0];
059
060    private final List<GpxTrack> lastTracks = new ArrayList<>(); // List of tracks at last paint
061    private int lastUpdateCount;
062
063    private final GpxDrawHelper drawHelper;
064
065    /**
066     * Constructs a new {@code GpxLayer} without name.
067     * @param d GPX data
068     */
069    public GpxLayer(GpxData d) {
070        this(d, null, false);
071    }
072
073    /**
074     * Constructs a new {@code GpxLayer} with a given name.
075     * @param d GPX data
076     * @param name layer name
077     */
078    public GpxLayer(GpxData d, String name) {
079        this(d, name, false);
080    }
081
082    /**
083     * Constructs a new {@code GpxLayer} with a given name, thah can be attached to a local file.
084     * @param d GPX data
085     * @param name layer name
086     * @param isLocal whether data is attached to a local file
087     */
088    public GpxLayer(GpxData d, String name, boolean isLocal) {
089        super(d.getString(GpxConstants.META_NAME));
090        data = d;
091        drawHelper = new GpxDrawHelper(data);
092        SystemOfMeasurement.addSoMChangeListener(drawHelper);
093        ensureTrackVisibilityLength();
094        setName(name);
095        isLocalFile = isLocal;
096    }
097
098    @Override
099    public Color getColor(boolean ignoreCustom) {
100        return drawHelper.getColor(getName(), ignoreCustom);
101    }
102
103    /**
104     * Returns a human readable string that shows the timespan of the given track
105     * @param trk The GPX track for which timespan is displayed
106     * @return The timespan as a string
107     */
108    public static String getTimespanForTrack(GpxTrack trk) {
109        Date[] bounds = GpxData.getMinMaxTimeForTrack(trk);
110        String ts = "";
111        if (bounds != null) {
112            DateFormat df = DateUtils.getDateFormat(DateFormat.SHORT);
113            String earliestDate = df.format(bounds[0]);
114            String latestDate = df.format(bounds[1]);
115
116            if (earliestDate.equals(latestDate)) {
117                DateFormat tf = DateUtils.getTimeFormat(DateFormat.SHORT);
118                ts += earliestDate + ' ';
119                ts += tf.format(bounds[0]) + " - " + tf.format(bounds[1]);
120            } else {
121                DateFormat dtf = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.MEDIUM);
122                ts += dtf.format(bounds[0]) + " - " + dtf.format(bounds[1]);
123            }
124
125            int diff = (int) (bounds[1].getTime() - bounds[0].getTime()) / 1000;
126            ts += String.format(" (%d:%02d)", diff / 3600, (diff % 3600) / 60);
127        }
128        return ts;
129    }
130
131    @Override
132    public Icon getIcon() {
133        return ImageProvider.get("layer", "gpx_small");
134    }
135
136    @Override
137    public Object getInfoComponent() {
138        StringBuilder info = new StringBuilder(48).append("<html>");
139
140        if (data.attr.containsKey("name")) {
141            info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>");
142        }
143
144        if (data.attr.containsKey("desc")) {
145            info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>");
146        }
147
148        if (!data.tracks.isEmpty()) {
149            info.append("<table><thead align='center'><tr><td colspan='5'>")
150                .append(trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size()))
151                .append("</td></tr><tr align='center'><td>").append(tr("Name")).append("</td><td>")
152                .append(tr("Description")).append("</td><td>").append(tr("Timespan"))
153                .append("</td><td>").append(tr("Length")).append("</td><td>").append(tr("URL"))
154                .append("</td></tr></thead>");
155
156            for (GpxTrack trk : data.tracks) {
157                info.append("<tr><td>");
158                if (trk.getAttributes().containsKey(GpxConstants.GPX_NAME)) {
159                    info.append(trk.get(GpxConstants.GPX_NAME));
160                }
161                info.append("</td><td>");
162                if (trk.getAttributes().containsKey(GpxConstants.GPX_DESC)) {
163                    info.append(' ').append(trk.get(GpxConstants.GPX_DESC));
164                }
165                info.append("</td><td>");
166                info.append(getTimespanForTrack(trk));
167                info.append("</td><td>");
168                info.append(SystemOfMeasurement.getSystemOfMeasurement().getDistText(trk.length()));
169                info.append("</td><td>");
170                if (trk.getAttributes().containsKey("url")) {
171                    info.append(trk.get("url"));
172                }
173                info.append("</td></tr>");
174            }
175            info.append("</table><br><br>");
176        }
177
178        info.append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length()))).append("<br>")
179            .append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())).append(
180                trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>")
181            .append("</html>");
182
183        final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()));
184        sp.setPreferredSize(new Dimension(sp.getPreferredSize().width+20, 370));
185        SwingUtilities.invokeLater(new Runnable() {
186            @Override
187            public void run() {
188                sp.getVerticalScrollBar().setValue(0);
189            }
190        });
191        return sp;
192    }
193
194    @Override
195    public boolean isInfoResizable() {
196        return true;
197    }
198
199    @Override
200    public Action[] getMenuEntries() {
201        return new Action[] {
202                LayerListDialog.getInstance().createShowHideLayerAction(),
203                LayerListDialog.getInstance().createDeleteLayerAction(),
204                LayerListDialog.getInstance().createMergeLayerAction(this),
205                SeparatorLayerAction.INSTANCE,
206                new LayerSaveAction(this),
207                new LayerSaveAsAction(this),
208                new CustomizeColor(this),
209                new CustomizeDrawingAction(this),
210                new ImportImagesAction(this),
211                new ImportAudioAction(this),
212                new MarkersFromNamedPointsAction(this),
213                new ConvertToDataLayerAction.FromGpxLayer(this),
214                new DownloadAlongTrackAction(data),
215                new DownloadWmsAlongTrackAction(data),
216                SeparatorLayerAction.INSTANCE,
217                new ChooseTrackVisibilityAction(this),
218                new RenameLayerAction(getAssociatedFile(), this),
219                SeparatorLayerAction.INSTANCE,
220                new LayerListPopup.InfoAction(this) };
221    }
222
223    /**
224     * Determines if data is attached to a local file.
225     * @return {@code true} if data is attached to a local file, {@code false} otherwise
226     */
227    public boolean isLocalFile() {
228        return isLocalFile;
229    }
230
231    @Override
232    public String getToolTipText() {
233        StringBuilder info = new StringBuilder(48).append("<html>");
234
235        if (data.attr.containsKey(GpxConstants.META_NAME)) {
236            info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>");
237        }
238
239        if (data.attr.containsKey(GpxConstants.META_DESC)) {
240            info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>");
241        }
242
243        info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size()))
244            .append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size()))
245            .append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>")
246            .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length())))
247            .append("<br></html>");
248        return info.toString();
249    }
250
251    @Override
252    public boolean isMergable(Layer other) {
253        return other instanceof GpxLayer;
254    }
255
256    private int sumUpdateCount() {
257        int updateCount = 0;
258        for (GpxTrack track: data.tracks) {
259            updateCount += track.getUpdateCount();
260        }
261        return updateCount;
262    }
263
264    @Override
265    public boolean isChanged() {
266        if (data.tracks.equals(lastTracks))
267            return sumUpdateCount() != lastUpdateCount;
268        else
269            return true;
270    }
271
272    public void filterTracksByDate(Date fromDate, Date toDate, boolean showWithoutDate) {
273        int i = 0;
274        long from = fromDate.getTime();
275        long to = toDate.getTime();
276        for (GpxTrack trk : data.tracks) {
277            Date[] t = GpxData.getMinMaxTimeForTrack(trk);
278
279            if (t == null) continue;
280            long tm = t[1].getTime();
281            trackVisibility[i] = (tm == 0 && showWithoutDate) || (from <= tm && tm <= to);
282            i++;
283        }
284    }
285
286    @Override
287    public void mergeFrom(Layer from) {
288        data.mergeFrom(((GpxLayer) from).data);
289        drawHelper.dataChanged();
290    }
291
292    @Override
293    public void paint(Graphics2D g, MapView mv, Bounds box) {
294        lastUpdateCount = sumUpdateCount();
295        lastTracks.clear();
296        lastTracks.addAll(data.tracks);
297
298        List<WayPoint> visibleSegments = listVisibleSegments(box);
299        if (!visibleSegments.isEmpty()) {
300            drawHelper.readPreferences(getName());
301            drawHelper.drawAll(g, mv, visibleSegments);
302            if (Main.getLayerManager().getActiveLayer() == this) {
303                drawHelper.drawColorBar(g, mv);
304            }
305        }
306    }
307
308    private List<WayPoint> listVisibleSegments(Bounds box) {
309        WayPoint last = null;
310        LinkedList<WayPoint> visibleSegments = new LinkedList<>();
311
312        ensureTrackVisibilityLength();
313        for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) {
314
315            for (WayPoint pt : segment) {
316                Bounds b = new Bounds(pt.getCoor());
317                if (pt.drawLine && last != null) {
318                    b.extend(last.getCoor());
319                }
320                if (b.intersects(box)) {
321                    if (last != null && (visibleSegments.isEmpty()
322                            || visibleSegments.getLast() != last)) {
323                        if (last.drawLine) {
324                            WayPoint l = new WayPoint(last);
325                            l.drawLine = false;
326                            visibleSegments.add(l);
327                        } else {
328                            visibleSegments.add(last);
329                        }
330                    }
331                    visibleSegments.add(pt);
332                }
333                last = pt;
334            }
335        }
336        return visibleSegments;
337    }
338
339    @Override
340    public void visitBoundingBox(BoundingXYVisitor v) {
341        v.visit(data.recalculateBounds());
342    }
343
344    @Override
345    public File getAssociatedFile() {
346        return data.storageFile;
347    }
348
349    @Override
350    public void setAssociatedFile(File file) {
351        data.storageFile = file;
352    }
353
354    /** ensures the trackVisibility array has the correct length without losing data.
355     * additional entries are initialized to true;
356     */
357    private void ensureTrackVisibilityLength() {
358        final int l = data.tracks.size();
359        if (l == trackVisibility.length)
360            return;
361        final int m = Math.min(l, trackVisibility.length);
362        trackVisibility = Arrays.copyOf(trackVisibility, l);
363        for (int i = m; i < l; i++) {
364            trackVisibility[i] = true;
365        }
366    }
367
368    @Override
369    public void projectionChanged(Projection oldValue, Projection newValue) {
370        if (newValue == null) return;
371        data.resetEastNorthCache();
372    }
373
374    @Override
375    public boolean isSavable() {
376        return true; // With GpxExporter
377    }
378
379    @Override
380    public boolean checkSaveConditions() {
381        return data != null;
382    }
383
384    @Override
385    public File createAndOpenSaveFileChooser() {
386        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.getFileFilter());
387    }
388
389    @Override
390    public LayerPositionStrategy getDefaultLayerPosition() {
391        return LayerPositionStrategy.AFTER_LAST_DATA_LAYER;
392    }
393
394    @Override
395    public void destroy() {
396        SystemOfMeasurement.removeSoMChangeListener(drawHelper);
397    }
398}