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