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