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.Dimension;
008import java.awt.Graphics2D;
009import java.io.File;
010import java.text.DateFormat;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.Collection;
014import java.util.Date;
015import java.util.LinkedList;
016import java.util.List;
017
018import javax.swing.Action;
019import javax.swing.Icon;
020import javax.swing.JScrollPane;
021import javax.swing.SwingUtilities;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.actions.RenameLayerAction;
025import org.openstreetmap.josm.actions.SaveActionBase;
026import org.openstreetmap.josm.data.Bounds;
027import org.openstreetmap.josm.data.SystemOfMeasurement;
028import org.openstreetmap.josm.data.gpx.GpxConstants;
029import org.openstreetmap.josm.data.gpx.GpxData;
030import org.openstreetmap.josm.data.gpx.GpxTrack;
031import org.openstreetmap.josm.data.gpx.WayPoint;
032import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
033import org.openstreetmap.josm.data.preferences.ColorProperty;
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, getColorProperty());
092        SystemOfMeasurement.addSoMChangeListener(drawHelper);
093        ensureTrackVisibilityLength();
094        setName(name);
095        isLocalFile = isLocal;
096    }
097
098    @Override
099    protected ColorProperty getBaseColorProperty() {
100        return GpxDrawHelper.DEFAULT_COLOR;
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()))
180            .append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br></html>");
181
182        final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()));
183        sp.setPreferredSize(new Dimension(sp.getPreferredSize().width+20, 370));
184        SwingUtilities.invokeLater(() -> sp.getVerticalScrollBar().setValue(0));
185        return sp;
186    }
187
188    @Override
189    public boolean isInfoResizable() {
190        return true;
191    }
192
193    @Override
194    public Action[] getMenuEntries() {
195        return new Action[] {
196                LayerListDialog.getInstance().createShowHideLayerAction(),
197                LayerListDialog.getInstance().createDeleteLayerAction(),
198                LayerListDialog.getInstance().createMergeLayerAction(this),
199                SeparatorLayerAction.INSTANCE,
200                new LayerSaveAction(this),
201                new LayerSaveAsAction(this),
202                new CustomizeColor(this),
203                new CustomizeDrawingAction(this),
204                new ImportImagesAction(this),
205                new ImportAudioAction(this),
206                new MarkersFromNamedPointsAction(this),
207                new ConvertToDataLayerAction.FromGpxLayer(this),
208                new DownloadAlongTrackAction(data),
209                new DownloadWmsAlongTrackAction(data),
210                SeparatorLayerAction.INSTANCE,
211                new ChooseTrackVisibilityAction(this),
212                new RenameLayerAction(getAssociatedFile(), this),
213                SeparatorLayerAction.INSTANCE,
214                new LayerListPopup.InfoAction(this) };
215    }
216
217    /**
218     * Determines if data is attached to a local file.
219     * @return {@code true} if data is attached to a local file, {@code false} otherwise
220     */
221    public boolean isLocalFile() {
222        return isLocalFile;
223    }
224
225    @Override
226    public String getToolTipText() {
227        StringBuilder info = new StringBuilder(48).append("<html>");
228
229        if (data.attr.containsKey(GpxConstants.META_NAME)) {
230            info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>");
231        }
232
233        if (data.attr.containsKey(GpxConstants.META_DESC)) {
234            info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>");
235        }
236
237        info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size()))
238            .append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size()))
239            .append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>")
240            .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length())))
241            .append("<br></html>");
242        return info.toString();
243    }
244
245    @Override
246    public boolean isMergable(Layer other) {
247        return other instanceof GpxLayer;
248    }
249
250    private int sumUpdateCount() {
251        int updateCount = 0;
252        for (GpxTrack track: data.tracks) {
253            updateCount += track.getUpdateCount();
254        }
255        return updateCount;
256    }
257
258    @Override
259    public boolean isChanged() {
260        if (data.tracks.equals(lastTracks))
261            return sumUpdateCount() != lastUpdateCount;
262        else
263            return true;
264    }
265
266    public void filterTracksByDate(Date fromDate, Date toDate, boolean showWithoutDate) {
267        int i = 0;
268        long from = fromDate.getTime();
269        long to = toDate.getTime();
270        for (GpxTrack trk : data.tracks) {
271            Date[] t = GpxData.getMinMaxTimeForTrack(trk);
272
273            if (t == null) continue;
274            long tm = t[1].getTime();
275            trackVisibility[i] = (tm == 0 && showWithoutDate) || (from <= tm && tm <= to);
276            i++;
277        }
278    }
279
280    @Override
281    public void mergeFrom(Layer from) {
282        if (!(from instanceof GpxLayer))
283            throw new IllegalArgumentException("not a GpxLayer: " + from);
284        data.mergeFrom(((GpxLayer) from).data);
285        drawHelper.dataChanged();
286    }
287
288    @Override
289    public void paint(Graphics2D g, MapView mv, Bounds box) {
290        lastUpdateCount = sumUpdateCount();
291        lastTracks.clear();
292        lastTracks.addAll(data.tracks);
293
294        List<WayPoint> visibleSegments = listVisibleSegments(box);
295        if (!visibleSegments.isEmpty()) {
296            drawHelper.readPreferences(getName());
297            drawHelper.drawAll(g, mv, visibleSegments);
298            if (Main.getLayerManager().getActiveLayer() == this) {
299                drawHelper.drawColorBar(g, mv);
300            }
301        }
302    }
303
304    private List<WayPoint> listVisibleSegments(Bounds box) {
305        WayPoint last = null;
306        LinkedList<WayPoint> visibleSegments = new LinkedList<>();
307
308        ensureTrackVisibilityLength();
309        for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) {
310
311            for (WayPoint pt : segment) {
312                Bounds b = new Bounds(pt.getCoor());
313                if (pt.drawLine && last != null) {
314                    b.extend(last.getCoor());
315                }
316                if (b.intersects(box)) {
317                    if (last != null && (visibleSegments.isEmpty()
318                            || visibleSegments.getLast() != last)) {
319                        if (last.drawLine) {
320                            WayPoint l = new WayPoint(last);
321                            l.drawLine = false;
322                            visibleSegments.add(l);
323                        } else {
324                            visibleSegments.add(last);
325                        }
326                    }
327                    visibleSegments.add(pt);
328                }
329                last = pt;
330            }
331        }
332        return visibleSegments;
333    }
334
335    @Override
336    public void visitBoundingBox(BoundingXYVisitor v) {
337        v.visit(data.recalculateBounds());
338    }
339
340    @Override
341    public File getAssociatedFile() {
342        return data.storageFile;
343    }
344
345    @Override
346    public void setAssociatedFile(File file) {
347        data.storageFile = file;
348    }
349
350    /** ensures the trackVisibility array has the correct length without losing data.
351     * additional entries are initialized to true;
352     */
353    private void ensureTrackVisibilityLength() {
354        final int l = data.tracks.size();
355        if (l == trackVisibility.length)
356            return;
357        final int m = Math.min(l, trackVisibility.length);
358        trackVisibility = Arrays.copyOf(trackVisibility, l);
359        for (int i = m; i < l; i++) {
360            trackVisibility[i] = true;
361        }
362    }
363
364    @Override
365    public void projectionChanged(Projection oldValue, Projection newValue) {
366        if (newValue == null) return;
367        data.resetEastNorthCache();
368    }
369
370    @Override
371    public boolean isSavable() {
372        return true; // With GpxExporter
373    }
374
375    @Override
376    public boolean checkSaveConditions() {
377        return data != null;
378    }
379
380    @Override
381    public File createAndOpenSaveFileChooser() {
382        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.getFileFilter());
383    }
384
385    @Override
386    public LayerPositionStrategy getDefaultLayerPosition() {
387        return LayerPositionStrategy.AFTER_LAST_DATA_LAYER;
388    }
389
390    @Override
391    public void destroy() {
392        super.destroy();
393        SystemOfMeasurement.removeSoMChangeListener(drawHelper);
394    }
395}