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