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.Dimension; 008import java.awt.Graphics2D; 009import java.io.File; 010import java.text.DateFormat; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.Date; 015import java.util.LinkedList; 016import java.util.List; 017 018import javax.swing.Action; 019import javax.swing.Icon; 020import javax.swing.JScrollPane; 021import javax.swing.SwingUtilities; 022 023import org.openstreetmap.josm.Main; 024import org.openstreetmap.josm.actions.RenameLayerAction; 025import org.openstreetmap.josm.actions.SaveActionBase; 026import org.openstreetmap.josm.data.Bounds; 027import org.openstreetmap.josm.data.SystemOfMeasurement; 028import org.openstreetmap.josm.data.gpx.GpxConstants; 029import org.openstreetmap.josm.data.gpx.GpxData; 030import org.openstreetmap.josm.data.gpx.GpxTrack; 031import org.openstreetmap.josm.data.gpx.WayPoint; 032import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 033import org.openstreetmap.josm.data.preferences.ColorProperty; 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, getColorProperty()); 092 SystemOfMeasurement.addSoMChangeListener(drawHelper); 093 ensureTrackVisibilityLength(); 094 setName(name); 095 isLocalFile = isLocal; 096 } 097 098 @Override 099 protected ColorProperty getBaseColorProperty() { 100 return GpxDrawHelper.DEFAULT_COLOR; 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())) 180 .append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br></html>"); 181 182 final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString())); 183 sp.setPreferredSize(new Dimension(sp.getPreferredSize().width+20, 370)); 184 SwingUtilities.invokeLater(() -> sp.getVerticalScrollBar().setValue(0)); 185 return sp; 186 } 187 188 @Override 189 public boolean isInfoResizable() { 190 return true; 191 } 192 193 @Override 194 public Action[] getMenuEntries() { 195 return new Action[] { 196 LayerListDialog.getInstance().createShowHideLayerAction(), 197 LayerListDialog.getInstance().createDeleteLayerAction(), 198 LayerListDialog.getInstance().createMergeLayerAction(this), 199 SeparatorLayerAction.INSTANCE, 200 new LayerSaveAction(this), 201 new LayerSaveAsAction(this), 202 new CustomizeColor(this), 203 new CustomizeDrawingAction(this), 204 new ImportImagesAction(this), 205 new ImportAudioAction(this), 206 new MarkersFromNamedPointsAction(this), 207 new ConvertToDataLayerAction.FromGpxLayer(this), 208 new DownloadAlongTrackAction(data), 209 new DownloadWmsAlongTrackAction(data), 210 SeparatorLayerAction.INSTANCE, 211 new ChooseTrackVisibilityAction(this), 212 new RenameLayerAction(getAssociatedFile(), this), 213 SeparatorLayerAction.INSTANCE, 214 new LayerListPopup.InfoAction(this) }; 215 } 216 217 /** 218 * Determines if data is attached to a local file. 219 * @return {@code true} if data is attached to a local file, {@code false} otherwise 220 */ 221 public boolean isLocalFile() { 222 return isLocalFile; 223 } 224 225 @Override 226 public String getToolTipText() { 227 StringBuilder info = new StringBuilder(48).append("<html>"); 228 229 if (data.attr.containsKey(GpxConstants.META_NAME)) { 230 info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>"); 231 } 232 233 if (data.attr.containsKey(GpxConstants.META_DESC)) { 234 info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>"); 235 } 236 237 info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size())) 238 .append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())) 239 .append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>") 240 .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length()))) 241 .append("<br></html>"); 242 return info.toString(); 243 } 244 245 @Override 246 public boolean isMergable(Layer other) { 247 return other instanceof GpxLayer; 248 } 249 250 private int sumUpdateCount() { 251 int updateCount = 0; 252 for (GpxTrack track: data.tracks) { 253 updateCount += track.getUpdateCount(); 254 } 255 return updateCount; 256 } 257 258 @Override 259 public boolean isChanged() { 260 if (data.tracks.equals(lastTracks)) 261 return sumUpdateCount() != lastUpdateCount; 262 else 263 return true; 264 } 265 266 public void filterTracksByDate(Date fromDate, Date toDate, boolean showWithoutDate) { 267 int i = 0; 268 long from = fromDate.getTime(); 269 long to = toDate.getTime(); 270 for (GpxTrack trk : data.tracks) { 271 Date[] t = GpxData.getMinMaxTimeForTrack(trk); 272 273 if (t == null) continue; 274 long tm = t[1].getTime(); 275 trackVisibility[i] = (tm == 0 && showWithoutDate) || (from <= tm && tm <= to); 276 i++; 277 } 278 } 279 280 @Override 281 public void mergeFrom(Layer from) { 282 if (!(from instanceof GpxLayer)) 283 throw new IllegalArgumentException("not a GpxLayer: " + from); 284 data.mergeFrom(((GpxLayer) from).data); 285 drawHelper.dataChanged(); 286 } 287 288 @Override 289 public void paint(Graphics2D g, MapView mv, Bounds box) { 290 lastUpdateCount = sumUpdateCount(); 291 lastTracks.clear(); 292 lastTracks.addAll(data.tracks); 293 294 List<WayPoint> visibleSegments = listVisibleSegments(box); 295 if (!visibleSegments.isEmpty()) { 296 drawHelper.readPreferences(getName()); 297 drawHelper.drawAll(g, mv, visibleSegments); 298 if (Main.getLayerManager().getActiveLayer() == this) { 299 drawHelper.drawColorBar(g, mv); 300 } 301 } 302 } 303 304 private List<WayPoint> listVisibleSegments(Bounds box) { 305 WayPoint last = null; 306 LinkedList<WayPoint> visibleSegments = new LinkedList<>(); 307 308 ensureTrackVisibilityLength(); 309 for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) { 310 311 for (WayPoint pt : segment) { 312 Bounds b = new Bounds(pt.getCoor()); 313 if (pt.drawLine && last != null) { 314 b.extend(last.getCoor()); 315 } 316 if (b.intersects(box)) { 317 if (last != null && (visibleSegments.isEmpty() 318 || visibleSegments.getLast() != last)) { 319 if (last.drawLine) { 320 WayPoint l = new WayPoint(last); 321 l.drawLine = false; 322 visibleSegments.add(l); 323 } else { 324 visibleSegments.add(last); 325 } 326 } 327 visibleSegments.add(pt); 328 } 329 last = pt; 330 } 331 } 332 return visibleSegments; 333 } 334 335 @Override 336 public void visitBoundingBox(BoundingXYVisitor v) { 337 v.visit(data.recalculateBounds()); 338 } 339 340 @Override 341 public File getAssociatedFile() { 342 return data.storageFile; 343 } 344 345 @Override 346 public void setAssociatedFile(File file) { 347 data.storageFile = file; 348 } 349 350 /** ensures the trackVisibility array has the correct length without losing data. 351 * additional entries are initialized to true; 352 */ 353 private void ensureTrackVisibilityLength() { 354 final int l = data.tracks.size(); 355 if (l == trackVisibility.length) 356 return; 357 final int m = Math.min(l, trackVisibility.length); 358 trackVisibility = Arrays.copyOf(trackVisibility, l); 359 for (int i = m; i < l; i++) { 360 trackVisibility[i] = true; 361 } 362 } 363 364 @Override 365 public void projectionChanged(Projection oldValue, Projection newValue) { 366 if (newValue == null) return; 367 data.resetEastNorthCache(); 368 } 369 370 @Override 371 public boolean isSavable() { 372 return true; // With GpxExporter 373 } 374 375 @Override 376 public boolean checkSaveConditions() { 377 return data != null; 378 } 379 380 @Override 381 public File createAndOpenSaveFileChooser() { 382 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.getFileFilter()); 383 } 384 385 @Override 386 public LayerPositionStrategy getDefaultLayerPosition() { 387 return LayerPositionStrategy.AFTER_LAST_DATA_LAYER; 388 } 389 390 @Override 391 public void destroy() { 392 super.destroy(); 393 SystemOfMeasurement.removeSoMChangeListener(drawHelper); 394 } 395}