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