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