// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.layer; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.Dimension; import java.awt.Graphics2D; import java.io.File; import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.LinkedList; import java.util.List; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.RenameLayerAction; import org.openstreetmap.josm.actions.SaveActionBase; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.SystemOfMeasurement; import org.openstreetmap.josm.data.gpx.GpxConstants; import org.openstreetmap.josm.data.gpx.GpxData; import org.openstreetmap.josm.data.gpx.GpxTrack; import org.openstreetmap.josm.data.gpx.WayPoint; import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; import org.openstreetmap.josm.data.preferences.ColorProperty; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.dialogs.LayerListDialog; import org.openstreetmap.josm.gui.dialogs.LayerListPopup; import org.openstreetmap.josm.gui.layer.gpx.ChooseTrackVisibilityAction; import org.openstreetmap.josm.gui.layer.gpx.ConvertToDataLayerAction; import org.openstreetmap.josm.gui.layer.gpx.CustomizeDrawingAction; import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongTrackAction; import org.openstreetmap.josm.gui.layer.gpx.DownloadWmsAlongTrackAction; import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper; import org.openstreetmap.josm.gui.layer.gpx.ImportAudioAction; import org.openstreetmap.josm.gui.layer.gpx.ImportImagesAction; import org.openstreetmap.josm.gui.layer.gpx.MarkersFromNamedPointsAction; import org.openstreetmap.josm.gui.widgets.HtmlPanel; import org.openstreetmap.josm.io.GpxImporter; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.date.DateUtils; public class GpxLayer extends Layer { /** GPX data */ public GpxData data; private final boolean isLocalFile; // used by ChooseTrackVisibilityAction to determine which tracks to show/hide public boolean[] trackVisibility = new boolean[0]; private final List<GpxTrack> lastTracks = new ArrayList<>(); // List of tracks at last paint private int lastUpdateCount; private final GpxDrawHelper drawHelper; /** * Constructs a new {@code GpxLayer} without name. * @param d GPX data */ public GpxLayer(GpxData d) { this(d, null, false); } /** * Constructs a new {@code GpxLayer} with a given name. * @param d GPX data * @param name layer name */ public GpxLayer(GpxData d, String name) { this(d, name, false); } /** * Constructs a new {@code GpxLayer} with a given name, thah can be attached to a local file. * @param d GPX data * @param name layer name * @param isLocal whether data is attached to a local file */ public GpxLayer(GpxData d, String name, boolean isLocal) { super(d.getString(GpxConstants.META_NAME)); data = d; drawHelper = new GpxDrawHelper(data); SystemOfMeasurement.addSoMChangeListener(drawHelper); ensureTrackVisibilityLength(); setName(name); isLocalFile = isLocal; } @Override protected ColorProperty getBaseColorProperty() { return GpxDrawHelper.DEFAULT_COLOR; } /** * Returns a human readable string that shows the timespan of the given track * @param trk The GPX track for which timespan is displayed * @return The timespan as a string */ public static String getTimespanForTrack(GpxTrack trk) { Date[] bounds = GpxData.getMinMaxTimeForTrack(trk); String ts = ""; if (bounds != null) { DateFormat df = DateUtils.getDateFormat(DateFormat.SHORT); String earliestDate = df.format(bounds[0]); String latestDate = df.format(bounds[1]); if (earliestDate.equals(latestDate)) { DateFormat tf = DateUtils.getTimeFormat(DateFormat.SHORT); ts += earliestDate + ' '; ts += tf.format(bounds[0]) + " - " + tf.format(bounds[1]); } else { DateFormat dtf = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.MEDIUM); ts += dtf.format(bounds[0]) + " - " + dtf.format(bounds[1]); } int diff = (int) (bounds[1].getTime() - bounds[0].getTime()) / 1000; ts += String.format(" (%d:%02d)", diff / 3600, (diff % 3600) / 60); } return ts; } @Override public Icon getIcon() { return ImageProvider.get("layer", "gpx_small"); } @Override public Object getInfoComponent() { StringBuilder info = new StringBuilder(48).append("<html>"); if (data.attr.containsKey("name")) { info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>"); } if (data.attr.containsKey("desc")) { info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>"); } if (!data.tracks.isEmpty()) { info.append("<table><thead align='center'><tr><td colspan='5'>") .append(trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size())) .append("</td></tr><tr align='center'><td>").append(tr("Name")).append("</td><td>") .append(tr("Description")).append("</td><td>").append(tr("Timespan")) .append("</td><td>").append(tr("Length")).append("</td><td>").append(tr("URL")) .append("</td></tr></thead>"); for (GpxTrack trk : data.tracks) { info.append("<tr><td>"); if (trk.getAttributes().containsKey(GpxConstants.GPX_NAME)) { info.append(trk.get(GpxConstants.GPX_NAME)); } info.append("</td><td>"); if (trk.getAttributes().containsKey(GpxConstants.GPX_DESC)) { info.append(' ').append(trk.get(GpxConstants.GPX_DESC)); } info.append("</td><td>"); info.append(getTimespanForTrack(trk)); info.append("</td><td>"); info.append(SystemOfMeasurement.getSystemOfMeasurement().getDistText(trk.length())); info.append("</td><td>"); if (trk.getAttributes().containsKey("url")) { info.append(trk.get("url")); } info.append("</td></tr>"); } info.append("</table><br><br>"); } info.append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length()))).append("<br>") .append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())) .append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br></html>"); final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString())); sp.setPreferredSize(new Dimension(sp.getPreferredSize().width+20, 370)); SwingUtilities.invokeLater(() -> sp.getVerticalScrollBar().setValue(0)); return sp; } @Override public boolean isInfoResizable() { return true; } @Override public Action[] getMenuEntries() { return new Action[] { LayerListDialog.getInstance().createShowHideLayerAction(), LayerListDialog.getInstance().createDeleteLayerAction(), LayerListDialog.getInstance().createMergeLayerAction(this), SeparatorLayerAction.INSTANCE, new LayerSaveAction(this), new LayerSaveAsAction(this), new CustomizeColor(this), new CustomizeDrawingAction(this), new ImportImagesAction(this), new ImportAudioAction(this), new MarkersFromNamedPointsAction(this), new ConvertToDataLayerAction.FromGpxLayer(this), new DownloadAlongTrackAction(data), new DownloadWmsAlongTrackAction(data), SeparatorLayerAction.INSTANCE, new ChooseTrackVisibilityAction(this), new RenameLayerAction(getAssociatedFile(), this), SeparatorLayerAction.INSTANCE, new LayerListPopup.InfoAction(this) }; } /** * Determines if data is attached to a local file. * @return {@code true} if data is attached to a local file, {@code false} otherwise */ public boolean isLocalFile() { return isLocalFile; } @Override public String getToolTipText() { StringBuilder info = new StringBuilder(48).append("<html>"); if (data.attr.containsKey(GpxConstants.META_NAME)) { info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>"); } if (data.attr.containsKey(GpxConstants.META_DESC)) { info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>"); } info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size())) .append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())) .append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>") .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length()))) .append("<br></html>"); return info.toString(); } @Override public boolean isMergable(Layer other) { return other instanceof GpxLayer; } private int sumUpdateCount() { int updateCount = 0; for (GpxTrack track: data.tracks) { updateCount += track.getUpdateCount(); } return updateCount; } @Override public boolean isChanged() { if (data.tracks.equals(lastTracks)) return sumUpdateCount() != lastUpdateCount; else return true; } public void filterTracksByDate(Date fromDate, Date toDate, boolean showWithoutDate) { int i = 0; long from = fromDate.getTime(); long to = toDate.getTime(); for (GpxTrack trk : data.tracks) { Date[] t = GpxData.getMinMaxTimeForTrack(trk); if (t == null) continue; long tm = t[1].getTime(); trackVisibility[i] = (tm == 0 && showWithoutDate) || (from <= tm && tm <= to); i++; } } @Override public void mergeFrom(Layer from) { if (!(from instanceof GpxLayer)) throw new IllegalArgumentException("not a GpxLayer: " + from); data.mergeFrom(((GpxLayer) from).data); drawHelper.dataChanged(); } @Override public void paint(Graphics2D g, MapView mv, Bounds box) { lastUpdateCount = sumUpdateCount(); lastTracks.clear(); lastTracks.addAll(data.tracks); List<WayPoint> visibleSegments = listVisibleSegments(box); if (!visibleSegments.isEmpty()) { drawHelper.readPreferences(getName()); drawHelper.drawAll(g, mv, visibleSegments); if (Main.getLayerManager().getActiveLayer() == this) { drawHelper.drawColorBar(g, mv); } } } private List<WayPoint> listVisibleSegments(Bounds box) { WayPoint last = null; LinkedList<WayPoint> visibleSegments = new LinkedList<>(); ensureTrackVisibilityLength(); for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) { for (WayPoint pt : segment) { Bounds b = new Bounds(pt.getCoor()); if (pt.drawLine && last != null) { b.extend(last.getCoor()); } if (b.intersects(box)) { if (last != null && (visibleSegments.isEmpty() || visibleSegments.getLast() != last)) { if (last.drawLine) { WayPoint l = new WayPoint(last); l.drawLine = false; visibleSegments.add(l); } else { visibleSegments.add(last); } } visibleSegments.add(pt); } last = pt; } } return visibleSegments; } @Override public void visitBoundingBox(BoundingXYVisitor v) { v.visit(data.recalculateBounds()); } @Override public File getAssociatedFile() { return data.storageFile; } @Override public void setAssociatedFile(File file) { data.storageFile = file; } /** ensures the trackVisibility array has the correct length without losing data. * additional entries are initialized to true; */ private void ensureTrackVisibilityLength() { final int l = data.tracks.size(); if (l == trackVisibility.length) return; final int m = Math.min(l, trackVisibility.length); trackVisibility = Arrays.copyOf(trackVisibility, l); for (int i = m; i < l; i++) { trackVisibility[i] = true; } } @Override public void projectionChanged(Projection oldValue, Projection newValue) { if (newValue == null) return; data.resetEastNorthCache(); } @Override public boolean isSavable() { return true; // With GpxExporter } @Override public boolean checkSaveConditions() { return data != null; } @Override public File createAndOpenSaveFileChooser() { return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.getFileFilter()); } @Override public LayerPositionStrategy getDefaultLayerPosition() { return LayerPositionStrategy.AFTER_LAST_DATA_LAYER; } @Override public void destroy() { super.destroy(); SystemOfMeasurement.removeSoMChangeListener(drawHelper); } }