001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.gpx; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.BasicStroke; 008import java.awt.Color; 009import java.awt.Graphics2D; 010import java.awt.Point; 011import java.awt.RenderingHints; 012import java.awt.Stroke; 013import java.util.Collection; 014import java.util.Date; 015import java.util.List; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.data.coor.LatLon; 019import org.openstreetmap.josm.data.gpx.GpxConstants; 020import org.openstreetmap.josm.data.gpx.GpxData; 021import org.openstreetmap.josm.data.gpx.WayPoint; 022import org.openstreetmap.josm.gui.MapView; 023import org.openstreetmap.josm.tools.ColorScale; 024 025/** 026 * Class that helps to draw large set of GPS tracks with different colors and options 027 * @since 7319 028 */ 029public class GpxDrawHelper { 030 private GpxData data; 031 032 // draw lines between points belonging to different segments 033 private boolean forceLines; 034 // draw direction arrows on the lines 035 private boolean direction; 036 /** don't draw lines if longer than x meters **/ 037 private int lineWidth; 038 private int maxLineLength; 039 private boolean lines; 040 /** paint large dots for points **/ 041 private boolean large; 042 private int largesize; 043 private boolean hdopCircle; 044 /** paint direction arrow with alternate math. may be faster **/ 045 private boolean alternateDirection; 046 /** don't draw arrows nearer to each other than this **/ 047 private int delta; 048 private double minTrackDurationForTimeColoring; 049 050 private int hdopfactor; 051 052 private static final double PHI = Math.toRadians(15); 053 054 //// Variables used only to check cache validity 055 private boolean computeCacheInSync; 056 private int computeCacheMaxLineLengthUsed; 057 private Color computeCacheColorUsed; 058 private boolean computeCacheColorDynamic; 059 private ColorMode computeCacheColored; 060 private int computeCacheColorTracksTune; 061 062 //// Color-related fields 063 /** Mode of the line coloring **/ 064 private ColorMode colored; 065 /** max speed for coloring - allows to tweak line coloring for different speed levels. **/ 066 private int colorTracksTune; 067 private boolean colorModeDynamic; 068 private Color neutralColor; 069 private int largePointAlpha; 070 071 // default access is used to allow changing from plugins 072 private ColorScale velocityScale; 073 /** Colors (without custom alpha channel, if given) for HDOP painting. **/ 074 private ColorScale hdopScale; 075 private ColorScale dateScale; 076 private ColorScale directionScale; 077 078 /** Opacity for hdop points **/ 079 private int hdopAlpha; 080 081 082 // lookup array to draw arrows without doing any math 083 private static final int ll0 = 9; 084 private static final int sl4 = 5; 085 private static final int sl9 = 3; 086 private static final int[][] dir = { 087 {+sl4, +ll0, +ll0, +sl4}, {-sl9, +ll0, +sl9, +ll0}, {-ll0, +sl4, -sl4, +ll0}, 088 {-ll0, -sl9, -ll0, +sl9}, {-sl4, -ll0, -ll0, -sl4}, {+sl9, -ll0, -sl9, -ll0}, 089 {+ll0, -sl4, +sl4, -ll0}, {+ll0, +sl9, +ll0, -sl9}, {+sl4, +ll0, +ll0, +sl4}, 090 {-sl9, +ll0, +sl9, +ll0}, {-ll0, +sl4, -sl4, +ll0}, {-ll0, -sl9, -ll0, +sl9}}; 091 092 private void setupColors() { 093 hdopAlpha = Main.pref.getInteger("hdop.color.alpha", -1); 094 velocityScale = ColorScale.createHSBScale(256).addTitle(tr("Velocity, km/h")); 095 /** Colors (without custom alpha channel, if given) for HDOP painting. **/ 096 hdopScale = ColorScale.createHSBScale(256).makeReversed().addTitle(tr("HDOP, m")); 097 dateScale = ColorScale.createHSBScale(256).addTitle(tr("Time")); 098 directionScale = ColorScale.createCyclicScale(256).setIntervalCount(4).addTitle(tr("Direction")); 099 } 100 101 /** 102 * Different color modes 103 */ 104 public enum ColorMode { 105 NONE, VELOCITY, HDOP, DIRECTION, TIME 106 } 107 108 /** 109 * Constructs a new {@code GpxDrawHelper}. 110 * @param gpxData GPX data 111 */ 112 public GpxDrawHelper(GpxData gpxData) { 113 data = gpxData; 114 setupColors(); 115 } 116 117 private static String specName(String layerName) { 118 return "layer " + layerName; 119 } 120 121 /** 122 * Get the default color for gps tracks for specified layer 123 * @param layerName name of the GpxLayer 124 * @param ignoreCustom do not use preferences 125 * @return the color or null if the color is not constant 126 */ 127 public Color getColor(String layerName, boolean ignoreCustom) { 128 Color c = Main.pref.getColor(marktr("gps point"), specName(layerName), Color.gray); 129 return ignoreCustom || getColorMode(layerName) == ColorMode.NONE ? c : null; 130 } 131 132 /** 133 * Read coloring mode for specified layer from preferences 134 * @param layerName name of the GpxLayer 135 * @return coloting mode 136 */ 137 public ColorMode getColorMode(String layerName) { 138 try { 139 int i = Main.pref.getInteger("draw.rawgps.colors", specName(layerName), 0); 140 return ColorMode.values()[i]; 141 } catch (Exception e) { 142 Main.warn(e); 143 } 144 return ColorMode.NONE; 145 } 146 147 /** Reads generic color from preferences (usually gray) 148 * @return the color 149 **/ 150 public static Color getGenericColor() { 151 return Main.pref.getColor(marktr("gps point"), Color.gray); 152 } 153 154 /** 155 * Read all drawing-related settings from preferences 156 * @param layerName layer name used to access its specific preferences 157 **/ 158 public void readPreferences(String layerName) { 159 String spec = specName(layerName); 160 forceLines = Main.pref.getBoolean("draw.rawgps.lines.force", spec, false); 161 direction = Main.pref.getBoolean("draw.rawgps.direction", spec, false); 162 lineWidth = Main.pref.getInteger("draw.rawgps.linewidth", spec, 0); 163 164 if (!data.fromServer) { 165 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", spec, -1); 166 lines = Main.pref.getBoolean("draw.rawgps.lines.local", spec, true); 167 } else { 168 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", spec, 200); 169 lines = Main.pref.getBoolean("draw.rawgps.lines", spec, true); 170 } 171 large = Main.pref.getBoolean("draw.rawgps.large", spec, false); 172 largesize = Main.pref.getInteger("draw.rawgps.large.size", spec, 3); 173 hdopCircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", spec, false); 174 colored = getColorMode(layerName); 175 alternateDirection = Main.pref.getBoolean("draw.rawgps.alternatedirection", spec, false); 176 delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", spec, 40); 177 colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", spec, 45); 178 colorModeDynamic = Main.pref.getBoolean("draw.rawgps.colors.dynamic", spec, false); 179 hdopfactor = Main.pref.getInteger("hdop.factor", 25); 180 minTrackDurationForTimeColoring = Main.pref.getInteger("draw.rawgps.date-coloring-min-dt", 60); 181 largePointAlpha = Main.pref.getInteger("draw.rawgps.large.alpha", -1) & 0xFF; 182 183 neutralColor = getColor(layerName, true); 184 velocityScale.setNoDataColor(neutralColor); 185 dateScale.setNoDataColor(neutralColor); 186 hdopScale.setNoDataColor(neutralColor); 187 directionScale.setNoDataColor(neutralColor); 188 189 largesize += lineWidth; 190 } 191 192 public void drawAll(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) { 193 194 checkCache(); 195 196 // STEP 2b - RE-COMPUTE CACHE DATA ********************* 197 if (!computeCacheInSync) { // don't compute if the cache is good 198 calculateColors(); 199 } 200 201 Stroke storedStroke = g.getStroke(); 202 203 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 204 Main.pref.getBoolean("mappaint.gpx.use-antialiasing", false) ? 205 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 206 207 if (lineWidth != 0) { 208 g.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); 209 } 210 fixColors(visibleSegments); 211 drawLines(g, mv, visibleSegments); 212 drawArrows(g, mv, visibleSegments); 213 drawPoints(g, mv, visibleSegments); 214 if (lineWidth != 0) { 215 g.setStroke(storedStroke); 216 } 217 } 218 219 public void calculateColors() { 220 double minval = +1e10; 221 double maxval = -1e10; 222 WayPoint oldWp = null; 223 224 if (colorModeDynamic) { 225 if (colored == ColorMode.VELOCITY) { 226 for (Collection<WayPoint> segment : data.getLinesIterable(null)) { 227 if (!forceLines) { 228 oldWp = null; 229 } 230 for (WayPoint trkPnt : segment) { 231 LatLon c = trkPnt.getCoor(); 232 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 233 continue; 234 } 235 if (oldWp != null && trkPnt.time > oldWp.time) { 236 double vel = c.greatCircleDistance(oldWp.getCoor()) 237 / (trkPnt.time - oldWp.time); 238 if (vel > maxval) { 239 maxval = vel; 240 } 241 if (vel < minval) { 242 minval = vel; 243 } 244 } 245 oldWp = trkPnt; 246 } 247 } 248 if (minval >= maxval) { 249 velocityScale.setRange(0, 120/3.6); 250 } else { 251 velocityScale.setRange(minval, maxval); 252 } 253 } else if (colored == ColorMode.HDOP) { 254 for (Collection<WayPoint> segment : data.getLinesIterable(null)) { 255 for (WayPoint trkPnt : segment) { 256 Object val = trkPnt.get(GpxConstants.PT_HDOP); 257 if (val != null) { 258 double hdop = ((Float) val).doubleValue(); 259 if (hdop > maxval) { 260 maxval = hdop; 261 } 262 if (hdop < minval) { 263 minval = hdop; 264 } 265 } 266 } 267 } 268 if (minval >= maxval) { 269 hdopScale.setRange(0, 100); 270 } else { 271 hdopScale.setRange(minval, maxval); 272 } 273 } 274 oldWp = null; 275 } else { // color mode not dynamic 276 velocityScale.setRange(0, colorTracksTune); 277 hdopScale.setRange(0, 1.0/hdopfactor); 278 } 279 double now = System.currentTimeMillis()/1000.0; 280 if (colored == ColorMode.TIME) { 281 Date[] bounds = data.getMinMaxTimeForAllTracks(); 282 if (bounds.length >= 2) { 283 minval = bounds[0].getTime()/1000.0; 284 maxval = bounds[1].getTime()/1000.0; 285 } else { 286 minval = 0; 287 maxval = now; 288 } 289 dateScale.setRange(minval, maxval); 290 } 291 292 293 // Now the colors for all the points will be assigned 294 for (Collection<WayPoint> segment : data.getLinesIterable(null)) { 295 if (!forceLines) { // don't draw lines between segments, unless forced to 296 oldWp = null; 297 } 298 for (WayPoint trkPnt : segment) { 299 LatLon c = trkPnt.getCoor(); 300 trkPnt.customColoring = neutralColor; 301 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 302 continue; 303 } 304 // now we are sure some color will be assigned 305 Color color = null; 306 307 if (colored == ColorMode.HDOP) { 308 Float hdop = (Float) trkPnt.get(GpxConstants.PT_HDOP); 309 color = hdopScale.getColor(hdop); 310 } 311 if (oldWp != null) { // other coloring modes need segment for calcuation 312 double dist = c.greatCircleDistance(oldWp.getCoor()); 313 boolean noDraw = false; 314 switch (colored) { 315 case VELOCITY: 316 double dtime = trkPnt.time - oldWp.time; 317 if (dtime > 0) { 318 color = velocityScale.getColor(dist / dtime); 319 } else { 320 color = velocityScale.getNoDataColor(); 321 } 322 break; 323 case DIRECTION: 324 double dirColor = oldWp.getCoor().heading(trkPnt.getCoor()); 325 color = directionScale.getColor(dirColor); 326 break; 327 case TIME: 328 double t = trkPnt.time; 329 // skip bad timestamps and very short tracks 330 if (t > 0 && t <= now && maxval - minval > minTrackDurationForTimeColoring) { 331 color = dateScale.getColor(t); 332 } else { 333 color = dateScale.getNoDataColor(); 334 } 335 break; 336 } 337 if (!noDraw && (maxLineLength == -1 || dist <= maxLineLength)) { 338 trkPnt.drawLine = true; 339 trkPnt.dir = (int) oldWp.getCoor().heading(trkPnt.getCoor()); 340 } else { 341 trkPnt.drawLine = false; 342 } 343 } else { // make sure we reset outdated data 344 trkPnt.drawLine = false; 345 color = neutralColor; 346 } 347 if (color != null) { 348 trkPnt.customColoring = color; 349 } 350 oldWp = trkPnt; 351 } 352 } 353 354 computeCacheInSync = true; 355 } 356 357 private void drawLines(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) { 358 if (lines) { 359 Point old = null; 360 for (WayPoint trkPnt : visibleSegments) { 361 LatLon c = trkPnt.getCoor(); 362 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 363 continue; 364 } 365 Point screen = mv.getPoint(trkPnt.getEastNorth()); 366 // skip points that are on the same screenposition 367 if (trkPnt.drawLine && old != null && ((old.x != screen.x) || (old.y != screen.y))) { 368 g.setColor(trkPnt.customColoring); 369 g.drawLine(old.x, old.y, screen.x, screen.y); 370 } 371 old = screen; 372 } 373 } 374 } 375 376 private void drawArrows(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) { 377 /**************************************************************** 378 ********** STEP 3b - DRAW NICE ARROWS ************************** 379 ****************************************************************/ 380 if (lines && direction && !alternateDirection) { 381 Point old = null; 382 Point oldA = null; // last arrow painted 383 for (WayPoint trkPnt : visibleSegments) { 384 LatLon c = trkPnt.getCoor(); 385 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 386 continue; 387 } 388 if (trkPnt.drawLine) { 389 Point screen = mv.getPoint(trkPnt.getEastNorth()); 390 // skip points that are on the same screenposition 391 if (old != null 392 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta 393 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) { 394 g.setColor(trkPnt.customColoring); 395 double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI; 396 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)), 397 (int) (screen.y + 10 * Math.sin(t - PHI))); 398 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)), 399 (int) (screen.y + 10 * Math.sin(t + PHI))); 400 oldA = screen; 401 } 402 old = screen; 403 } 404 } // end for trkpnt 405 } 406 407 /**************************************************************** 408 ********** STEP 3c - DRAW FAST ARROWS ************************** 409 ****************************************************************/ 410 if (lines && direction && alternateDirection) { 411 Point old = null; 412 Point oldA = null; // last arrow painted 413 for (WayPoint trkPnt : visibleSegments) { 414 LatLon c = trkPnt.getCoor(); 415 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 416 continue; 417 } 418 if (trkPnt.drawLine) { 419 Point screen = mv.getPoint(trkPnt.getEastNorth()); 420 // skip points that are on the same screenposition 421 if (old != null 422 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta 423 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) { 424 g.setColor(trkPnt.customColoring); 425 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y 426 + dir[trkPnt.dir][1]); 427 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y 428 + dir[trkPnt.dir][3]); 429 oldA = screen; 430 } 431 old = screen; 432 } 433 } // end for trkpnt 434 } 435 } 436 437 private void drawPoints(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) { 438 /**************************************************************** 439 ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE ********* 440 ****************************************************************/ 441 if (large || hdopCircle) { 442 final int halfSize = largesize/2; 443 for (WayPoint trkPnt : visibleSegments) { 444 LatLon c = trkPnt.getCoor(); 445 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 446 continue; 447 } 448 Point screen = mv.getPoint(trkPnt.getEastNorth()); 449 450 451 if (hdopCircle && trkPnt.get(GpxConstants.PT_HDOP) != null) { 452 // hdop value 453 float hdop = (Float) trkPnt.get(GpxConstants.PT_HDOP); 454 if (hdop < 0) { 455 hdop = 0; 456 } 457 Color customColoringTransparent = hdopAlpha < 0 ? trkPnt.customColoring : 458 new Color(trkPnt.customColoring.getRGB() & 0x00ffffff | hdopAlpha << 24, true); 459 g.setColor(customColoringTransparent); 460 // hdop cirles 461 int hdopp = mv.getPoint(new LatLon( 462 trkPnt.getCoor().lat(), 463 trkPnt.getCoor().lon() + 2*6*hdop*360/40000000d)).x - screen.x; 464 g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360); 465 } 466 if (large) { 467 // color the large GPS points like the gps lines 468 if (trkPnt.customColoring != null) { 469 Color customColoringTransparent = largePointAlpha < 0 ? trkPnt.customColoring : 470 new Color(trkPnt.customColoring.getRGB() & 0x00ffffff | largePointAlpha << 24, true); 471 472 g.setColor(customColoringTransparent); 473 } 474 g.fillRect(screen.x-halfSize, screen.y-halfSize, largesize, largesize); 475 } 476 } // end for trkpnt 477 } // end if large || hdopcircle 478 479 /**************************************************************** 480 ********** STEP 3e - DRAW SMALL POINTS FOR LINES *************** 481 ****************************************************************/ 482 if (!large && lines) { 483 g.setColor(neutralColor); 484 for (WayPoint trkPnt : visibleSegments) { 485 LatLon c = trkPnt.getCoor(); 486 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 487 continue; 488 } 489 if (!trkPnt.drawLine) { 490 Point screen = mv.getPoint(trkPnt.getEastNorth()); 491 g.drawRect(screen.x, screen.y, 0, 0); 492 } 493 } // end for trkpnt 494 } // end if large 495 496 /**************************************************************** 497 ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ******** 498 ****************************************************************/ 499 if (!large && !lines) { 500 g.setColor(neutralColor); 501 for (WayPoint trkPnt : visibleSegments) { 502 LatLon c = trkPnt.getCoor(); 503 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 504 continue; 505 } 506 Point screen = mv.getPoint(trkPnt.getEastNorth()); 507 g.setColor(trkPnt.customColoring); 508 g.drawRect(screen.x, screen.y, 0, 0); 509 } // end for trkpnt 510 } // end if large 511 } 512 513 private void fixColors(List<WayPoint> visibleSegments) { 514 for (WayPoint trkPnt : visibleSegments) { 515 if (trkPnt.customColoring == null) { 516 trkPnt.customColoring = neutralColor; 517 } 518 } 519 } 520 521 /** 522 * Check cache validity set necessary flags 523 */ 524 private void checkCache() { 525 if ((computeCacheMaxLineLengthUsed != maxLineLength) || (!neutralColor.equals(computeCacheColorUsed)) 526 || (computeCacheColored != colored) || (computeCacheColorTracksTune != colorTracksTune) 527 || (computeCacheColorDynamic != colorModeDynamic)) { 528 computeCacheMaxLineLengthUsed = maxLineLength; 529 computeCacheInSync = false; 530 computeCacheColorUsed = neutralColor; 531 computeCacheColored = colored; 532 computeCacheColorTracksTune = colorTracksTune; 533 computeCacheColorDynamic = colorModeDynamic; 534 } 535 } 536 537 public void dataChanged() { 538 computeCacheInSync = false; 539 } 540 541 public void drawColorBar(Graphics2D g, MapView mv) { 542 int w = mv.getWidth(); 543 if (colored == ColorMode.HDOP) { 544 hdopScale.drawColorBar(g, w-30, 50, 20, 100, 1.0); 545 } else if (colored == ColorMode.VELOCITY) { 546 velocityScale.drawColorBar(g, w-30, 50, 20, 100, 3.6); 547 } else if (colored == ColorMode.DIRECTION) { 548 directionScale.drawColorBar(g, w-30, 50, 20, 100, 180.0/Math.PI); 549 } 550 } 551}