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}