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