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