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