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