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((String) d.attr.get("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.attr.get(GpxConstants.META_NAME))).append("<br>");
127        }
128
129        if (data.attr.containsKey("desc")) {
130            info.append(tr("Description: {0}", data.attr.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("name")) {
144                    info.append(trk.getAttributes().get("name"));
145                }
146                info.append("</td><td>");
147                if (trk.getAttributes().containsKey("desc")) {
148                    info.append(" ").append(trk.getAttributes().get("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.getAttributes().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("name")) {
218            info.append(tr("Name: {0}", data.attr.get(GpxConstants.META_NAME))).append("<br>");
219        }
220
221        if (data.attr.containsKey("desc")) {
222            info.append(tr("Description: {0}", data.attr.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        LinkedList<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
293    private LinkedList<WayPoint> listVisibleSegments(Bounds box) {
294        WayPoint last = null;
295        LinkedList<WayPoint> visibleSegments = new LinkedList<>();
296
297        ensureTrackVisibilityLength();
298        for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) {
299
300            for(WayPoint pt : segment)
301            {
302                Bounds b = new Bounds(pt.getCoor());
303                // last should never be null when this is true!
304                if(pt.drawLine) {
305                    b.extend(last.getCoor());
306                }
307                if(b.intersects(box))
308                {
309                    if(last != null && (visibleSegments.isEmpty()
310                            || visibleSegments.getLast() != last)) {
311                        if(last.drawLine) {
312                            WayPoint l = new WayPoint(last);
313                            l.drawLine = false;
314                            visibleSegments.add(l);
315                        } else {
316                            visibleSegments.add(last);
317                        }
318                    }
319                    visibleSegments.add(pt);
320                }
321                last = pt;
322            }
323        }
324        return visibleSegments;
325    }
326
327    @Override
328    public void visitBoundingBox(BoundingXYVisitor v) {
329        v.visit(data.recalculateBounds());
330    }
331
332    @Override
333    public File getAssociatedFile() {
334        return data.storageFile;
335    }
336
337    @Override
338    public void setAssociatedFile(File file) {
339        data.storageFile = file;
340    }
341
342    /** ensures the trackVisibility array has the correct length without losing data.
343     * additional entries are initialized to true;
344     */
345    private void ensureTrackVisibilityLength() {
346        final int l = data.tracks.size();
347        if (l == trackVisibility.length)
348            return;
349        final int m = Math.min(l, trackVisibility.length);
350        trackVisibility = Arrays.copyOf(trackVisibility, l);
351        for (int i = m; i < l; i++) {
352            trackVisibility[i] = true;
353        }
354    }
355
356    @Override
357    public void projectionChanged(Projection oldValue, Projection newValue) {
358        if (newValue == null) return;
359        data.resetEastNorthCache();
360    }
361
362    @Override
363    public boolean isSavable() {
364        return true; // With GpxExporter
365    }
366
367    @Override
368    public boolean checkSaveConditions() {
369        return data != null;
370    }
371
372    @Override
373    public File createAndOpenSaveFileChooser() {
374        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.FILE_FILTER);
375    }
376
377}