001// License: GPL. See LICENSE file for details. 002 003package org.openstreetmap.josm.gui.layer; 004 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007import static org.openstreetmap.josm.tools.I18n.trn; 008 009import java.awt.BasicStroke; 010import java.awt.Color; 011import java.awt.Dimension; 012import java.awt.Graphics2D; 013import java.awt.Point; 014import java.awt.RenderingHints; 015import java.awt.Stroke; 016import java.io.File; 017import java.text.DateFormat; 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Date; 021import java.util.LinkedList; 022import java.util.List; 023 024import javax.swing.Action; 025import javax.swing.Icon; 026import javax.swing.JScrollPane; 027import javax.swing.SwingUtilities; 028 029import org.openstreetmap.josm.Main; 030import org.openstreetmap.josm.actions.RenameLayerAction; 031import org.openstreetmap.josm.actions.SaveActionBase; 032import org.openstreetmap.josm.data.Bounds; 033import org.openstreetmap.josm.data.coor.LatLon; 034import org.openstreetmap.josm.data.gpx.GpxConstants; 035import org.openstreetmap.josm.data.gpx.GpxData; 036import org.openstreetmap.josm.data.gpx.GpxRoute; 037import org.openstreetmap.josm.data.gpx.GpxTrack; 038import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 039import org.openstreetmap.josm.data.gpx.WayPoint; 040import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 041import org.openstreetmap.josm.data.projection.Projection; 042import org.openstreetmap.josm.gui.MapView; 043import org.openstreetmap.josm.gui.NavigatableComponent; 044import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 045import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 046import org.openstreetmap.josm.gui.layer.gpx.ChooseTrackVisibilityAction; 047import org.openstreetmap.josm.gui.layer.gpx.ConvertToDataLayerAction; 048import org.openstreetmap.josm.gui.layer.gpx.CustomizeDrawingAction; 049import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongTrackAction; 050import org.openstreetmap.josm.gui.layer.gpx.DownloadWmsAlongTrackAction; 051import org.openstreetmap.josm.gui.layer.gpx.ImportAudioAction; 052import org.openstreetmap.josm.gui.layer.gpx.ImportImagesAction; 053import org.openstreetmap.josm.gui.layer.gpx.MarkersFromNamedPointsAction; 054import org.openstreetmap.josm.gui.widgets.HtmlPanel; 055import org.openstreetmap.josm.io.GpxImporter; 056import org.openstreetmap.josm.tools.ImageProvider; 057import org.openstreetmap.josm.tools.Utils; 058 059public class GpxLayer extends Layer { 060 061 public GpxData data; 062 protected static final double PHI = Math.toRadians(15); 063 private boolean computeCacheInSync; 064 private int computeCacheMaxLineLengthUsed; 065 private Color computeCacheColorUsed; 066 private boolean computeCacheColorDynamic; 067 private colorModes computeCacheColored; 068 private int computeCacheColorTracksTune; 069 private boolean isLocalFile; 070 // used by ChooseTrackVisibilityAction to determine which tracks to show/hide 071 public boolean[] trackVisibility = new boolean[0]; 072 073 private final List<GpxTrack> lastTracks = new ArrayList<GpxTrack>(); // List of tracks at last paint 074 private int lastUpdateCount; 075 076 public GpxLayer(GpxData d) { 077 super((String) d.attr.get("name")); 078 data = d; 079 computeCacheInSync = false; 080 ensureTrackVisibilityLength(); 081 } 082 083 public GpxLayer(GpxData d, String name) { 084 this(d); 085 this.setName(name); 086 } 087 088 public GpxLayer(GpxData d, String name, boolean isLocal) { 089 this(d); 090 this.setName(name); 091 this.isLocalFile = isLocal; 092 } 093 094 /** 095 * returns minimum and maximum timestamps in the track 096 */ 097 public static Date[] getMinMaxTimeForTrack(GpxTrack trk) { 098 WayPoint earliest = null, latest = null; 099 100 for (GpxTrackSegment seg : trk.getSegments()) { 101 for (WayPoint pnt : seg.getWayPoints()) { 102 if (latest == null) { 103 latest = earliest = pnt; 104 } else { 105 if (pnt.compareTo(earliest) < 0) { 106 earliest = pnt; 107 } else { 108 latest = pnt; 109 } 110 } 111 } 112 } 113 if (earliest==null || latest==null) return null; 114 return new Date[]{earliest.getTime(), latest.getTime()}; 115 } 116 117 /** 118 * Returns minimum and maximum timestamps for all tracks 119 * Warning: there are lot of track with broken timestamps, 120 * so we just ingore points from future and from year before 1970 in this method 121 * works correctly @since 5815 122 */ 123 public Date[] getMinMaxTimeForAllTracks() { 124 double min=1e100, max=-1e100, t; 125 double now = System.currentTimeMillis()/1000.0; 126 for (GpxTrack trk: data.tracks) { 127 for (GpxTrackSegment seg : trk.getSegments()) { 128 for (WayPoint pnt : seg.getWayPoints()) { 129 t = pnt.time; 130 if (t>0 && t<=now) { 131 if (t>max) max=t; 132 if (t<min) min=t; 133 } 134 } 135 } 136 } 137 if (min==1e100 || max==-1e100) return null; 138 return new Date[]{new Date((long) (min * 1000)), new Date((long) (max * 1000)), }; 139 } 140 141 142 /** 143 * returns a human readable string that shows the timespan of the given track 144 */ 145 public static String getTimespanForTrack(GpxTrack trk) { 146 Date[] bounds = getMinMaxTimeForTrack(trk); 147 String ts = ""; 148 if (bounds != null) { 149 DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT); 150 String earliestDate = df.format(bounds[0]); 151 String latestDate = df.format(bounds[1]); 152 153 if (earliestDate.equals(latestDate)) { 154 DateFormat tf = DateFormat.getTimeInstance(DateFormat.SHORT); 155 ts += earliestDate + " "; 156 ts += tf.format(bounds[0]) + " - " + tf.format(bounds[1]); 157 } else { 158 DateFormat dtf = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); 159 ts += dtf.format(bounds[0]) + " - " + dtf.format(bounds[1]); 160 } 161 162 int diff = (int) (bounds[1].getTime() - bounds[0].getTime()) / 1000; 163 ts += String.format(" (%d:%02d)", diff / 3600, (diff % 3600) / 60); 164 } 165 return ts; 166 } 167 168 @Override 169 public Icon getIcon() { 170 return ImageProvider.get("layer", "gpx_small"); 171 } 172 173 @Override 174 public Object getInfoComponent() { 175 StringBuilder info = new StringBuilder(); 176 177 if (data.attr.containsKey("name")) { 178 info.append(tr("Name: {0}", data.attr.get(GpxConstants.META_NAME))).append("<br>"); 179 } 180 181 if (data.attr.containsKey("desc")) { 182 info.append(tr("Description: {0}", data.attr.get(GpxConstants.META_DESC))).append("<br>"); 183 } 184 185 if (!data.tracks.isEmpty()) { 186 info.append("<table><thead align='center'><tr><td colspan='5'>" 187 + trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size()) 188 + "</td></tr><tr align='center'><td>" + tr("Name") + "</td><td>" 189 + tr("Description") + "</td><td>" + tr("Timespan") 190 + "</td><td>" + tr("Length") + "</td><td>" + tr("URL") 191 + "</td></tr></thead>"); 192 193 for (GpxTrack trk : data.tracks) { 194 info.append("<tr><td>"); 195 if (trk.getAttributes().containsKey("name")) { 196 info.append(trk.getAttributes().get("name")); 197 } 198 info.append("</td><td>"); 199 if (trk.getAttributes().containsKey("desc")) { 200 info.append(" ").append(trk.getAttributes().get("desc")); 201 } 202 info.append("</td><td>"); 203 info.append(getTimespanForTrack(trk)); 204 info.append("</td><td>"); 205 info.append(NavigatableComponent.getSystemOfMeasurement().getDistText(trk.length())); 206 info.append("</td><td>"); 207 if (trk.getAttributes().containsKey("url")) { 208 info.append(trk.getAttributes().get("url")); 209 } 210 info.append("</td></tr>"); 211 } 212 213 info.append("</table><br><br>"); 214 215 } 216 217 info.append(tr("Length: {0}", NavigatableComponent.getSystemOfMeasurement().getDistText(data.length()))).append("<br>"); 218 219 info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())).append( 220 trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>"); 221 222 final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()), JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 223 sp.setPreferredSize(new Dimension(sp.getPreferredSize().width, 350)); 224 SwingUtilities.invokeLater(new Runnable() { 225 @Override 226 public void run() { 227 sp.getVerticalScrollBar().setValue(0); 228 } 229 }); 230 return sp; 231 } 232 233 @Override 234 public Color getColor(boolean ignoreCustom) { 235 Color c = Main.pref.getColor(marktr("gps point"), "layer " + getName(), Color.gray); 236 237 return ignoreCustom || getColorMode() == colorModes.none ? c : null; 238 } 239 240 public colorModes getColorMode() { 241 try { 242 int i=Main.pref.getInteger("draw.rawgps.colors", "layer " + getName(), 0); 243 return colorModes.values()[i]; 244 } catch (Exception e) { 245 Main.warn(e); 246 } 247 return colorModes.none; 248 } 249 250 /* for preferences */ 251 static public Color getGenericColor() { 252 return Main.pref.getColor(marktr("gps point"), Color.gray); 253 } 254 255 @Override 256 public Action[] getMenuEntries() { 257 if (Main.applet) { 258 return new Action[] { 259 LayerListDialog.getInstance().createShowHideLayerAction(), 260 LayerListDialog.getInstance().createDeleteLayerAction(), 261 SeparatorLayerAction.INSTANCE, 262 new CustomizeColor(this), 263 new CustomizeDrawingAction(this), 264 new ConvertToDataLayerAction(this), 265 SeparatorLayerAction.INSTANCE, 266 new ChooseTrackVisibilityAction(this), 267 new RenameLayerAction(getAssociatedFile(), this), 268 SeparatorLayerAction.INSTANCE, 269 new LayerListPopup.InfoAction(this) }; 270 } 271 return new Action[] { 272 LayerListDialog.getInstance().createShowHideLayerAction(), 273 LayerListDialog.getInstance().createDeleteLayerAction(), 274 SeparatorLayerAction.INSTANCE, 275 new LayerSaveAction(this), 276 new LayerSaveAsAction(this), 277 new CustomizeColor(this), 278 new CustomizeDrawingAction(this), 279 new ImportImagesAction(this), 280 new ImportAudioAction(this), 281 new MarkersFromNamedPointsAction(this), 282 new ConvertToDataLayerAction(this), 283 new DownloadAlongTrackAction(data), 284 new DownloadWmsAlongTrackAction(data), 285 SeparatorLayerAction.INSTANCE, 286 new ChooseTrackVisibilityAction(this), 287 new RenameLayerAction(getAssociatedFile(), this), 288 SeparatorLayerAction.INSTANCE, 289 new LayerListPopup.InfoAction(this) }; 290 } 291 292 public boolean isLocalFile() { 293 return isLocalFile; 294 } 295 296 @Override 297 public String getToolTipText() { 298 StringBuilder info = new StringBuilder().append("<html>"); 299 300 if (data.attr.containsKey("name")) { 301 info.append(tr("Name: {0}", data.attr.get(GpxConstants.META_NAME))).append("<br>"); 302 } 303 304 if (data.attr.containsKey("desc")) { 305 info.append(tr("Description: {0}", data.attr.get(GpxConstants.META_DESC))).append("<br>"); 306 } 307 308 info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size())); 309 info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())); 310 info.append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>"); 311 312 info.append(tr("Length: {0}", NavigatableComponent.getSystemOfMeasurement().getDistText(data.length()))); 313 info.append("<br>"); 314 315 return info.append("</html>").toString(); 316 } 317 318 @Override 319 public boolean isMergable(Layer other) { 320 return other instanceof GpxLayer; 321 } 322 323 private int sumUpdateCount() { 324 int updateCount = 0; 325 for (GpxTrack track: data.tracks) { 326 updateCount += track.getUpdateCount(); 327 } 328 return updateCount; 329 } 330 331 @Override 332 public boolean isChanged() { 333 if (data.tracks.equals(lastTracks)) 334 return sumUpdateCount() != lastUpdateCount; 335 else 336 return true; 337 } 338 339 public void filterTracksByDate(Date fromDate, Date toDate, boolean showWithoutDate) { 340 int i = 0; 341 long from = fromDate.getTime(); 342 long to = toDate.getTime(); 343 for (GpxTrack trk : data.tracks) { 344 Date[] t = GpxLayer.getMinMaxTimeForTrack(trk); 345 346 if (t==null) continue; 347 long tm = t[1].getTime(); 348 trackVisibility[i]= (tm==0 && showWithoutDate) || (from<=tm && tm <= to); 349 i++; 350 } 351 } 352 353 @Override 354 public void mergeFrom(Layer from) { 355 data.mergeFrom(((GpxLayer) from).data); 356 computeCacheInSync = false; 357 } 358 359 private final static Color[] colors = new Color[256]; 360 static { 361 for (int i = 0; i < colors.length; i++) { 362 colors[i] = Color.getHSBColor(i / 300.0f, 1, 1); 363 } 364 } 365 366 private final static Color[] colors_cyclic = new Color[256]; 367 static { 368 for (int i = 0; i < colors_cyclic.length; i++) { 369 // red yellow green blue red 370 int[] h = new int[] { 0, 59, 127, 244, 360}; 371 int[] s = new int[] { 100, 84, 99, 100 }; 372 int[] b = new int[] { 90, 93, 74, 83 }; 373 374 float angle = 4 - i / 256f * 4; 375 int quadrant = (int) angle; 376 angle -= quadrant; 377 quadrant = Utils.mod(quadrant+1, 4); 378 379 float vh = h[quadrant] * w(angle) + h[quadrant+1] * (1 - w(angle)); 380 float vs = s[quadrant] * w(angle) + s[Utils.mod(quadrant+1, 4)] * (1 - w(angle)); 381 float vb = b[quadrant] * w(angle) + b[Utils.mod(quadrant+1, 4)] * (1 - w(angle)); 382 383 colors_cyclic[i] = Color.getHSBColor(vh/360f, vs/100f, vb/100f); 384 } 385 } 386 387 /** 388 * transition function: 389 * w(0)=1, w(1)=0, 0<=w(x)<=1 390 * @param x number: 0<=x<=1 391 * @return the weighted value 392 */ 393 private static float w(float x) { 394 if (x < 0.5) 395 return 1 - 2*x*x; 396 else 397 return 2*(1-x)*(1-x); 398 } 399 400 // lookup array to draw arrows without doing any math 401 private final static int ll0 = 9; 402 private final static int sl4 = 5; 403 private final static int sl9 = 3; 404 private final static int[][] dir = { { +sl4, +ll0, +ll0, +sl4 }, { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, 405 { -ll0, -sl9, -ll0, +sl9 }, { -sl4, -ll0, -ll0, -sl4 }, { +sl9, -ll0, -sl9, -ll0 }, 406 { +ll0, -sl4, +sl4, -ll0 }, { +ll0, +sl9, +ll0, -sl9 }, { +sl4, +ll0, +ll0, +sl4 }, 407 { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, { -ll0, -sl9, -ll0, +sl9 } }; 408 409 // the different color modes 410 enum colorModes { 411 none, velocity, dilution, direction, time 412 } 413 414 @Override 415 public void paint(Graphics2D g, MapView mv, Bounds box) { 416 lastUpdateCount = sumUpdateCount(); 417 lastTracks.clear(); 418 lastTracks.addAll(data.tracks); 419 420 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 421 Main.pref.getBoolean("mappaint.gpx.use-antialiasing", false) ? 422 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 423 424 /**************************************************************** 425 ********** STEP 1 - GET CONFIG VALUES ************************** 426 ****************************************************************/ 427 Color neutralColor = getColor(true); 428 String spec="layer "+getName(); 429 430 // also draw lines between points belonging to different segments 431 boolean forceLines = Main.pref.getBoolean("draw.rawgps.lines.force", spec, false); 432 // draw direction arrows on the lines 433 boolean direction = Main.pref.getBoolean("draw.rawgps.direction", spec, false); 434 // don't draw lines if longer than x meters 435 int lineWidth = Main.pref.getInteger("draw.rawgps.linewidth", spec, 0); 436 437 int maxLineLength; 438 boolean lines; 439 if (!this.data.fromServer) { 440 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", spec, -1); 441 lines = Main.pref.getBoolean("draw.rawgps.lines.local", spec, true); 442 } else { 443 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", spec, 200); 444 lines = Main.pref.getBoolean("draw.rawgps.lines", spec, true); 445 } 446 // paint large dots for points 447 boolean large = Main.pref.getBoolean("draw.rawgps.large", spec, false); 448 int largesize = Main.pref.getInteger("draw.rawgps.large.size", spec, 3); 449 boolean hdopcircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", spec, false); 450 // color the lines 451 colorModes colored = getColorMode(); 452 // paint direction arrow with alternate math. may be faster 453 boolean alternatedirection = Main.pref.getBoolean("draw.rawgps.alternatedirection", spec, false); 454 // don't draw arrows nearer to each other than this 455 int delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", spec, 40); 456 // allows to tweak line coloring for different speed levels. 457 int colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", spec, 45); 458 boolean colorModeDynamic = Main.pref.getBoolean("draw.rawgps.colors.dynamic", spec, false); 459 int hdopfactor = Main.pref.getInteger("hdop.factor", 25); 460 461 Stroke storedStroke = g.getStroke(); 462 if(lineWidth != 0) 463 { 464 g.setStroke(new BasicStroke(lineWidth,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND)); 465 largesize += lineWidth; 466 } 467 468 /**************************************************************** 469 ********** STEP 2a - CHECK CACHE VALIDITY ********************** 470 ****************************************************************/ 471 if ((computeCacheMaxLineLengthUsed != maxLineLength) || (!neutralColor.equals(computeCacheColorUsed)) 472 || (computeCacheColored != colored) || (computeCacheColorTracksTune != colorTracksTune) 473 || (computeCacheColorDynamic != colorModeDynamic)) { 474 computeCacheMaxLineLengthUsed = maxLineLength; 475 computeCacheInSync = false; 476 computeCacheColorUsed = neutralColor; 477 computeCacheColored = colored; 478 computeCacheColorTracksTune = colorTracksTune; 479 computeCacheColorDynamic = colorModeDynamic; 480 } 481 482 /**************************************************************** 483 ********** STEP 2b - RE-COMPUTE CACHE DATA ********************* 484 ****************************************************************/ 485 if (!computeCacheInSync) { // don't compute if the cache is good 486 double minval = +1e10; 487 double maxval = -1e10; 488 WayPoint oldWp = null; 489 if (colorModeDynamic) { 490 if (colored == colorModes.velocity) { 491 for (Collection<WayPoint> segment : data.getLinesIterable(null)) { 492 if(!forceLines) { 493 oldWp = null; 494 } 495 for (WayPoint trkPnt : segment) { 496 LatLon c = trkPnt.getCoor(); 497 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 498 continue; 499 } 500 if (oldWp != null && trkPnt.time > oldWp.time) { 501 double vel = c.greatCircleDistance(oldWp.getCoor()) 502 / (trkPnt.time - oldWp.time); 503 if(vel > maxval) { 504 maxval = vel; 505 } 506 if(vel < minval) { 507 minval = vel; 508 } 509 } 510 oldWp = trkPnt; 511 } 512 } 513 } else if (colored == colorModes.dilution) { 514 for (Collection<WayPoint> segment : data.getLinesIterable(null)) { 515 for (WayPoint trkPnt : segment) { 516 Object val = trkPnt.attr.get("hdop"); 517 if (val != null) { 518 double hdop = ((Float) val).doubleValue(); 519 if(hdop > maxval) { 520 maxval = hdop; 521 } 522 if(hdop < minval) { 523 minval = hdop; 524 } 525 } 526 } 527 } 528 } 529 oldWp = null; 530 } 531 double now = System.currentTimeMillis()/1000.0; 532 if (colored == colorModes.time) { 533 Date[] bounds = getMinMaxTimeForAllTracks(); 534 if (bounds!=null) { 535 minval = bounds[0].getTime()/1000.0; 536 maxval = bounds[1].getTime()/1000.0; 537 } else { 538 minval = 0; maxval=now; 539 } 540 } 541 542 for (Collection<WayPoint> segment : data.getLinesIterable(null)) { 543 if (!forceLines) { // don't draw lines between segments, unless forced to 544 oldWp = null; 545 } 546 for (WayPoint trkPnt : segment) { 547 LatLon c = trkPnt.getCoor(); 548 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 549 continue; 550 } 551 trkPnt.customColoring = neutralColor; 552 if(colored == colorModes.dilution && trkPnt.attr.get("hdop") != null) { 553 float hdop = ((Float) trkPnt.attr.get("hdop")).floatValue(); 554 int hdoplvl =(int) Math.round(colorModeDynamic ? ((hdop-minval)*255/(maxval-minval)) 555 : (hdop <= 0 ? 0 : hdop * hdopfactor)); 556 // High hdop is bad, but high values in colors are green. 557 // Therefore inverse the logic 558 int hdopcolor = 255 - (hdoplvl > 255 ? 255 : hdoplvl); 559 trkPnt.customColoring = colors[hdopcolor]; 560 } 561 if (oldWp != null) { 562 double dist = c.greatCircleDistance(oldWp.getCoor()); 563 boolean noDraw=false; 564 switch (colored) { 565 case velocity: 566 double dtime = trkPnt.time - oldWp.time; 567 if(dtime > 0) { 568 float vel = (float) (dist / dtime); 569 int velColor =(int) Math.round(colorModeDynamic ? ((vel-minval)*255/(maxval-minval)) 570 : (vel <= 0 ? 0 : vel / colorTracksTune * 255)); 571 trkPnt.customColoring = colors[Math.max(0, Math.min(velColor, 255))]; 572 } else { 573 trkPnt.customColoring = colors[255]; 574 } 575 break; 576 case direction: 577 double dirColor = oldWp.getCoor().heading(trkPnt.getCoor()) / (2.0 * Math.PI) * 256; 578 // Bad case first 579 if (dirColor != dirColor || dirColor < 0.0 || dirColor >= 256.0) { 580 trkPnt.customColoring = colors_cyclic[0]; 581 } else { 582 trkPnt.customColoring = colors_cyclic[(int) (dirColor)]; 583 } 584 break; 585 case time: 586 double t=trkPnt.time; 587 if (t>0 && t<=now){ // skip bad timestamps 588 int tColor = (int) Math.round((t-minval)*255/(maxval-minval)); 589 trkPnt.customColoring = colors[tColor]; 590 } else { 591 trkPnt.customColoring = neutralColor; 592 } 593 break; 594 } 595 596 if (!noDraw && (maxLineLength == -1 || dist <= maxLineLength)) { 597 trkPnt.drawLine = true; 598 trkPnt.dir = (int) oldWp.getCoor().heading(trkPnt.getCoor()); 599 } else { 600 trkPnt.drawLine = false; 601 } 602 } else { // make sure we reset outdated data 603 trkPnt.drawLine = false; 604 } 605 oldWp = trkPnt; 606 } 607 } 608 computeCacheInSync = true; 609 } 610 611 LinkedList<WayPoint> visibleSegments = new LinkedList<WayPoint>(); 612 WayPoint last = null; 613 ensureTrackVisibilityLength(); 614 for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) { 615 616 for(WayPoint pt : segment) 617 { 618 Bounds b = new Bounds(pt.getCoor()); 619 // last should never be null when this is true! 620 if(pt.drawLine) { 621 b.extend(last.getCoor()); 622 } 623 if(b.intersects(box)) 624 { 625 if(last != null && (visibleSegments.isEmpty() 626 || visibleSegments.getLast() != last)) { 627 if(last.drawLine) { 628 WayPoint l = new WayPoint(last); 629 l.drawLine = false; 630 visibleSegments.add(l); 631 } else { 632 visibleSegments.add(last); 633 } 634 } 635 visibleSegments.add(pt); 636 } 637 last = pt; 638 } 639 } 640 if(visibleSegments.isEmpty()) 641 return; 642 643 /**************************************************************** 644 ********** STEP 3a - DRAW LINES ******************************** 645 ****************************************************************/ 646 if (lines) { 647 Point old = null; 648 for (WayPoint trkPnt : visibleSegments) { 649 LatLon c = trkPnt.getCoor(); 650 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 651 continue; 652 } 653 Point screen = mv.getPoint(trkPnt.getEastNorth()); 654 if (trkPnt.drawLine) { 655 // skip points that are on the same screenposition 656 if (old != null && ((old.x != screen.x) || (old.y != screen.y))) { 657 g.setColor(trkPnt.customColoring); 658 g.drawLine(old.x, old.y, screen.x, screen.y); 659 } 660 } 661 old = screen; 662 } // end for trkpnt 663 } // end if lines 664 665 /**************************************************************** 666 ********** STEP 3b - DRAW NICE ARROWS ************************** 667 ****************************************************************/ 668 if (lines && direction && !alternatedirection) { 669 Point old = null; 670 Point oldA = null; // last arrow painted 671 for (WayPoint trkPnt : visibleSegments) { 672 LatLon c = trkPnt.getCoor(); 673 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 674 continue; 675 } 676 if (trkPnt.drawLine) { 677 Point screen = mv.getPoint(trkPnt.getEastNorth()); 678 // skip points that are on the same screenposition 679 if (old != null 680 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta 681 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) { 682 g.setColor(trkPnt.customColoring); 683 double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI; 684 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)), 685 (int) (screen.y + 10 * Math.sin(t - PHI))); 686 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)), 687 (int) (screen.y + 10 * Math.sin(t + PHI))); 688 oldA = screen; 689 } 690 old = screen; 691 } 692 } // end for trkpnt 693 } // end if lines 694 695 /**************************************************************** 696 ********** STEP 3c - DRAW FAST ARROWS ************************** 697 ****************************************************************/ 698 if (lines && direction && alternatedirection) { 699 Point old = null; 700 Point oldA = null; // last arrow painted 701 for (WayPoint trkPnt : visibleSegments) { 702 LatLon c = trkPnt.getCoor(); 703 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 704 continue; 705 } 706 if (trkPnt.drawLine) { 707 Point screen = mv.getPoint(trkPnt.getEastNorth()); 708 // skip points that are on the same screenposition 709 if (old != null 710 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta 711 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) { 712 g.setColor(trkPnt.customColoring); 713 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y 714 + dir[trkPnt.dir][1]); 715 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y 716 + dir[trkPnt.dir][3]); 717 oldA = screen; 718 } 719 old = screen; 720 } 721 } // end for trkpnt 722 } // end if lines 723 724 /**************************************************************** 725 ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE ********* 726 ****************************************************************/ 727 if (large || hdopcircle) { 728 g.setColor(neutralColor); 729 for (WayPoint trkPnt : visibleSegments) { 730 LatLon c = trkPnt.getCoor(); 731 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 732 continue; 733 } 734 Point screen = mv.getPoint(trkPnt.getEastNorth()); 735 g.setColor(trkPnt.customColoring); 736 if (hdopcircle && trkPnt.attr.get("hdop") != null) { 737 // hdop value 738 float hdop = ((Float)trkPnt.attr.get("hdop")).floatValue(); 739 if (hdop < 0) { 740 hdop = 0; 741 } 742 // hdop pixels 743 int hdopp = mv.getPoint(new LatLon(trkPnt.getCoor().lat(), trkPnt.getCoor().lon() + 2*6*hdop*360/40000000)).x - screen.x; 744 g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360); 745 } 746 if (large) { 747 g.fillRect(screen.x-1, screen.y-1, largesize, largesize); 748 } 749 } // end for trkpnt 750 } // end if large || hdopcircle 751 752 /**************************************************************** 753 ********** STEP 3e - DRAW SMALL POINTS FOR LINES *************** 754 ****************************************************************/ 755 if (!large && lines) { 756 g.setColor(neutralColor); 757 for (WayPoint trkPnt : visibleSegments) { 758 LatLon c = trkPnt.getCoor(); 759 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 760 continue; 761 } 762 if (!trkPnt.drawLine) { 763 Point screen = mv.getPoint(trkPnt.getEastNorth()); 764 g.drawRect(screen.x, screen.y, 0, 0); 765 } 766 } // end for trkpnt 767 } // end if large 768 769 /**************************************************************** 770 ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ******** 771 ****************************************************************/ 772 if (!large && !lines) { 773 g.setColor(neutralColor); 774 for (WayPoint trkPnt : visibleSegments) { 775 LatLon c = trkPnt.getCoor(); 776 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 777 continue; 778 } 779 Point screen = mv.getPoint(trkPnt.getEastNorth()); 780 g.setColor(trkPnt.customColoring); 781 g.drawRect(screen.x, screen.y, 0, 0); 782 } // end for trkpnt 783 } // end if large 784 785 if(lineWidth != 0) 786 { 787 g.setStroke(storedStroke); 788 } 789 } // end paint 790 791 @Override 792 public void visitBoundingBox(BoundingXYVisitor v) { 793 v.visit(data.recalculateBounds()); 794 } 795 796 @Override 797 public File getAssociatedFile() { 798 return data.storageFile; 799 } 800 801 @Override 802 public void setAssociatedFile(File file) { 803 data.storageFile = file; 804 } 805 806 /** ensures the trackVisibility array has the correct length without losing data. 807 * additional entries are initialized to true; 808 */ 809 final private void ensureTrackVisibilityLength() { 810 final int l = data.tracks.size(); 811 if(l == trackVisibility.length) 812 return; 813 final boolean[] back = trackVisibility.clone(); 814 final int m = Math.min(l, back.length); 815 trackVisibility = new boolean[l]; 816 System.arraycopy(back, 0, trackVisibility, 0, m); 817 for(int i=m; i < l; i++) { 818 trackVisibility[i] = true; 819 } 820 } 821 822 @Override 823 public void projectionChanged(Projection oldValue, Projection newValue) { 824 if (newValue == null) return; 825 if (data.waypoints != null) { 826 for (WayPoint wp : data.waypoints){ 827 wp.invalidateEastNorthCache(); 828 } 829 } 830 if (data.tracks != null){ 831 for (GpxTrack track: data.tracks) { 832 for (GpxTrackSegment segment: track.getSegments()) { 833 for (WayPoint wp: segment.getWayPoints()) { 834 wp.invalidateEastNorthCache(); 835 } 836 } 837 } 838 } 839 if (data.routes != null) { 840 for (GpxRoute route: data.routes) { 841 if (route.routePoints == null) { 842 continue; 843 } 844 for (WayPoint wp: route.routePoints) { 845 wp.invalidateEastNorthCache(); 846 } 847 } 848 } 849 } 850 851 @Override 852 public boolean isSavable() { 853 return true; // With GpxExporter 854 } 855 856 @Override 857 public boolean checkSaveConditions() { 858 return data != null; 859 } 860 861 @Override 862 public File createAndOpenSaveFileChooser() { 863 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.FILE_FILTER); 864 } 865 866}