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        data.mergeFrom(((GpxLayer) from).data);
283        drawHelper.dataChanged();
284    }
285
286    @Override
287    public void paint(Graphics2D g, MapView mv, Bounds box) {
288        lastUpdateCount = sumUpdateCount();
289        lastTracks.clear();
290        lastTracks.addAll(data.tracks);
291
292        List<WayPoint> visibleSegments = listVisibleSegments(box);
293        if (!visibleSegments.isEmpty()) {
294            drawHelper.readPreferences(getName());
295            drawHelper.drawAll(g, mv, visibleSegments);
296            if (Main.getLayerManager().getActiveLayer() == this) {
297                drawHelper.drawColorBar(g, mv);
298            }
299        }
300    }
301
302    private List<WayPoint> listVisibleSegments(Bounds box) {
303        WayPoint last = null;
304        LinkedList<WayPoint> visibleSegments = new LinkedList<>();
305
306        ensureTrackVisibilityLength();
307        for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) {
308
309            for (WayPoint pt : segment) {
310                Bounds b = new Bounds(pt.getCoor());
311                if (pt.drawLine && last != null) {
312                    b.extend(last.getCoor());
313                }
314                if (b.intersects(box)) {
315                    if (last != null && (visibleSegments.isEmpty()
316                            || visibleSegments.getLast() != last)) {
317                        if (last.drawLine) {
318                            WayPoint l = new WayPoint(last);
319                            l.drawLine = false;
320                            visibleSegments.add(l);
321                        } else {
322                            visibleSegments.add(last);
323                        }
324                    }
325                    visibleSegments.add(pt);
326                }
327                last = pt;
328            }
329        }
330        return visibleSegments;
331    }
332
333    @Override
334    public void visitBoundingBox(BoundingXYVisitor v) {
335        v.visit(data.recalculateBounds());
336    }
337
338    @Override
339    public File getAssociatedFile() {
340        return data.storageFile;
341    }
342
343    @Override
344    public void setAssociatedFile(File file) {
345        data.storageFile = file;
346    }
347
348    /** ensures the trackVisibility array has the correct length without losing data.
349     * additional entries are initialized to true;
350     */
351    private void ensureTrackVisibilityLength() {
352        final int l = data.tracks.size();
353        if (l == trackVisibility.length)
354            return;
355        final int m = Math.min(l, trackVisibility.length);
356        trackVisibility = Arrays.copyOf(trackVisibility, l);
357        for (int i = m; i < l; i++) {
358            trackVisibility[i] = true;
359        }
360    }
361
362    @Override
363    public void projectionChanged(Projection oldValue, Projection newValue) {
364        if (newValue == null) return;
365        data.resetEastNorthCache();
366    }
367
368    @Override
369    public boolean isSavable() {
370        return true; // With GpxExporter
371    }
372
373    @Override
374    public boolean checkSaveConditions() {
375        return data != null;
376    }
377
378    @Override
379    public File createAndOpenSaveFileChooser() {
380        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.getFileFilter());
381    }
382
383    @Override
384    public LayerPositionStrategy getDefaultLayerPosition() {
385        return LayerPositionStrategy.AFTER_LAST_DATA_LAYER;
386    }
387
388    @Override
389    public void destroy() {
390        super.destroy();
391        SystemOfMeasurement.removeSoMChangeListener(drawHelper);
392    }
393}