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