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