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