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        ensureTrackVisibilityLength();
093        setName(name);
094        isLocalFile = isLocal;
095    }
096
097    @Override
098    public Color getColor(boolean ignoreCustom) {
099        return drawHelper.getColor(getName(), ignoreCustom);
100    }
101
102    /**
103     * Returns a human readable string that shows the timespan of the given track
104     * @param trk The GPX track for which timespan is displayed
105     * @return The timespan as a string
106     */
107    public static String getTimespanForTrack(GpxTrack trk) {
108        Date[] bounds = GpxData.getMinMaxTimeForTrack(trk);
109        String ts = "";
110        if (bounds != null) {
111            DateFormat df = DateUtils.getDateFormat(DateFormat.SHORT);
112            String earliestDate = df.format(bounds[0]);
113            String latestDate = df.format(bounds[1]);
114
115            if (earliestDate.equals(latestDate)) {
116                DateFormat tf = DateUtils.getTimeFormat(DateFormat.SHORT);
117                ts += earliestDate + ' ';
118                ts += tf.format(bounds[0]) + " - " + tf.format(bounds[1]);
119            } else {
120                DateFormat dtf = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.MEDIUM);
121                ts += dtf.format(bounds[0]) + " - " + dtf.format(bounds[1]);
122            }
123
124            int diff = (int) (bounds[1].getTime() - bounds[0].getTime()) / 1000;
125            ts += String.format(" (%d:%02d)", diff / 3600, (diff % 3600) / 60);
126        }
127        return ts;
128    }
129
130    @Override
131    public Icon getIcon() {
132        return ImageProvider.get("layer", "gpx_small");
133    }
134
135    @Override
136    public Object getInfoComponent() {
137        StringBuilder info = new StringBuilder(48);
138
139        if (data.attr.containsKey("name")) {
140            info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>");
141        }
142
143        if (data.attr.containsKey("desc")) {
144            info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>");
145        }
146
147        if (!data.tracks.isEmpty()) {
148            info.append("<table><thead align='center'><tr><td colspan='5'>")
149                .append(trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size()))
150                .append("</td></tr><tr align='center'><td>").append(tr("Name")).append("</td><td>")
151                .append(tr("Description")).append("</td><td>").append(tr("Timespan"))
152                .append("</td><td>").append(tr("Length")).append("</td><td>").append(tr("URL"))
153                .append("</td></tr></thead>");
154
155            for (GpxTrack trk : data.tracks) {
156                info.append("<tr><td>");
157                if (trk.getAttributes().containsKey(GpxConstants.GPX_NAME)) {
158                    info.append(trk.get(GpxConstants.GPX_NAME));
159                }
160                info.append("</td><td>");
161                if (trk.getAttributes().containsKey(GpxConstants.GPX_DESC)) {
162                    info.append(' ').append(trk.get(GpxConstants.GPX_DESC));
163                }
164                info.append("</td><td>");
165                info.append(getTimespanForTrack(trk));
166                info.append("</td><td>");
167                info.append(SystemOfMeasurement.getSystemOfMeasurement().getDistText(trk.length()));
168                info.append("</td><td>");
169                if (trk.getAttributes().containsKey("url")) {
170                    info.append(trk.get("url"));
171                }
172                info.append("</td></tr>");
173            }
174            info.append("</table><br><br>");
175        }
176
177        info.append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length()))).append("<br>")
178            .append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())).append(
179                trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>");
180
181        final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()));
182        sp.setPreferredSize(new Dimension(sp.getPreferredSize().width+20, 370));
183        SwingUtilities.invokeLater(new Runnable() {
184            @Override
185            public void run() {
186                sp.getVerticalScrollBar().setValue(0);
187            }
188        });
189        return sp;
190    }
191
192    @Override
193    public boolean isInfoResizable() {
194        return true;
195    }
196
197    @Override
198    public Action[] getMenuEntries() {
199        return new Action[] {
200                LayerListDialog.getInstance().createShowHideLayerAction(),
201                LayerListDialog.getInstance().createDeleteLayerAction(),
202                LayerListDialog.getInstance().createMergeLayerAction(this),
203                SeparatorLayerAction.INSTANCE,
204                new LayerSaveAction(this),
205                new LayerSaveAsAction(this),
206                new CustomizeColor(this),
207                new CustomizeDrawingAction(this),
208                new ImportImagesAction(this),
209                new ImportAudioAction(this),
210                new MarkersFromNamedPointsAction(this),
211                new ConvertToDataLayerAction.FromGpxLayer(this),
212                new DownloadAlongTrackAction(data),
213                new DownloadWmsAlongTrackAction(data),
214                SeparatorLayerAction.INSTANCE,
215                new ChooseTrackVisibilityAction(this),
216                new RenameLayerAction(getAssociatedFile(), this),
217                SeparatorLayerAction.INSTANCE,
218                new LayerListPopup.InfoAction(this) };
219    }
220
221    /**
222     * Determines if data is attached to a local file.
223     * @return {@code true} if data is attached to a local file, {@code false} otherwise
224     */
225    public boolean isLocalFile() {
226        return isLocalFile;
227    }
228
229    @Override
230    public String getToolTipText() {
231        StringBuilder info = new StringBuilder(48).append("<html>");
232
233        if (data.attr.containsKey(GpxConstants.META_NAME)) {
234            info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>");
235        }
236
237        if (data.attr.containsKey(GpxConstants.META_DESC)) {
238            info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>");
239        }
240
241        info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size()))
242            .append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size()))
243            .append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>")
244            .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length())))
245            .append("<br></html>");
246        return info.toString();
247    }
248
249    @Override
250    public boolean isMergable(Layer other) {
251        return other instanceof GpxLayer;
252    }
253
254    private int sumUpdateCount() {
255        int updateCount = 0;
256        for (GpxTrack track: data.tracks) {
257            updateCount += track.getUpdateCount();
258        }
259        return updateCount;
260    }
261
262    @Override
263    public boolean isChanged() {
264        if (data.tracks.equals(lastTracks))
265            return sumUpdateCount() != lastUpdateCount;
266        else
267            return true;
268    }
269
270    public void filterTracksByDate(Date fromDate, Date toDate, boolean showWithoutDate) {
271        int i = 0;
272        long from = fromDate.getTime();
273        long to = toDate.getTime();
274        for (GpxTrack trk : data.tracks) {
275            Date[] t = GpxData.getMinMaxTimeForTrack(trk);
276
277            if (t == null) continue;
278            long tm = t[1].getTime();
279            trackVisibility[i] = (tm == 0 && showWithoutDate) || (from <= tm && tm <= to);
280            i++;
281        }
282    }
283
284    @Override
285    public void mergeFrom(Layer from) {
286        data.mergeFrom(((GpxLayer) from).data);
287        drawHelper.dataChanged();
288    }
289
290    @Override
291    public void paint(Graphics2D g, MapView mv, Bounds box) {
292        lastUpdateCount = sumUpdateCount();
293        lastTracks.clear();
294        lastTracks.addAll(data.tracks);
295
296        List<WayPoint> visibleSegments = listVisibleSegments(box);
297        if (!visibleSegments.isEmpty()) {
298            drawHelper.readPreferences(getName());
299            drawHelper.drawAll(g, mv, visibleSegments);
300            if (Main.map.mapView.getActiveLayer() == this) {
301                drawHelper.drawColorBar(g, mv);
302            }
303        }
304    }
305
306    private List<WayPoint> listVisibleSegments(Bounds box) {
307        WayPoint last = null;
308        LinkedList<WayPoint> visibleSegments = new LinkedList<>();
309
310        ensureTrackVisibilityLength();
311        for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) {
312
313            for (WayPoint pt : segment) {
314                Bounds b = new Bounds(pt.getCoor());
315                if (pt.drawLine && last != null) {
316                    b.extend(last.getCoor());
317                }
318                if (b.intersects(box)) {
319                    if (last != null && (visibleSegments.isEmpty()
320                            || visibleSegments.getLast() != last)) {
321                        if (last.drawLine) {
322                            WayPoint l = new WayPoint(last);
323                            l.drawLine = false;
324                            visibleSegments.add(l);
325                        } else {
326                            visibleSegments.add(last);
327                        }
328                    }
329                    visibleSegments.add(pt);
330                }
331                last = pt;
332            }
333        }
334        return visibleSegments;
335    }
336
337    @Override
338    public void visitBoundingBox(BoundingXYVisitor v) {
339        v.visit(data.recalculateBounds());
340    }
341
342    @Override
343    public File getAssociatedFile() {
344        return data.storageFile;
345    }
346
347    @Override
348    public void setAssociatedFile(File file) {
349        data.storageFile = file;
350    }
351
352    /** ensures the trackVisibility array has the correct length without losing data.
353     * additional entries are initialized to true;
354     */
355    private void ensureTrackVisibilityLength() {
356        final int l = data.tracks.size();
357        if (l == trackVisibility.length)
358            return;
359        final int m = Math.min(l, trackVisibility.length);
360        trackVisibility = Arrays.copyOf(trackVisibility, l);
361        for (int i = m; i < l; i++) {
362            trackVisibility[i] = true;
363        }
364    }
365
366    @Override
367    public void projectionChanged(Projection oldValue, Projection newValue) {
368        if (newValue == null) return;
369        data.resetEastNorthCache();
370    }
371
372    @Override
373    public boolean isSavable() {
374        return true; // With GpxExporter
375    }
376
377    @Override
378    public boolean checkSaveConditions() {
379        return data != null;
380    }
381
382    @Override
383    public File createAndOpenSaveFileChooser() {
384        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.getFileFilter());
385    }
386
387}