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.AlphaComposite; 008import java.awt.BasicStroke; 009import java.awt.Color; 010import java.awt.Composite; 011import java.awt.Graphics2D; 012import java.awt.LinearGradientPaint; 013import java.awt.MultipleGradientPaint; 014import java.awt.Paint; 015import java.awt.Point; 016import java.awt.Rectangle; 017import java.awt.RenderingHints; 018import java.awt.Stroke; 019import java.awt.image.BufferedImage; 020import java.awt.image.DataBufferInt; 021import java.awt.image.Raster; 022import java.io.BufferedReader; 023import java.io.IOException; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Collections; 027import java.util.Date; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Objects; 031import java.util.Random; 032 033import javax.swing.ImageIcon; 034 035import org.openstreetmap.josm.data.Bounds; 036import org.openstreetmap.josm.data.SystemOfMeasurement; 037import org.openstreetmap.josm.data.SystemOfMeasurement.SoMChangeListener; 038import org.openstreetmap.josm.data.coor.LatLon; 039import org.openstreetmap.josm.data.gpx.GpxConstants; 040import org.openstreetmap.josm.data.gpx.GpxData; 041import org.openstreetmap.josm.data.gpx.GpxData.GpxDataChangeEvent; 042import org.openstreetmap.josm.data.gpx.GpxData.GpxDataChangeListener; 043import org.openstreetmap.josm.data.gpx.Line; 044import org.openstreetmap.josm.data.gpx.WayPoint; 045import org.openstreetmap.josm.data.preferences.NamedColorProperty; 046import org.openstreetmap.josm.gui.MapView; 047import org.openstreetmap.josm.gui.MapViewState; 048import org.openstreetmap.josm.gui.layer.GpxLayer; 049import org.openstreetmap.josm.gui.layer.MapViewGraphics; 050import org.openstreetmap.josm.gui.layer.MapViewPaintable; 051import org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent; 052import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationEvent; 053import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationListener; 054import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel; 055import org.openstreetmap.josm.io.CachedFile; 056import org.openstreetmap.josm.spi.preferences.Config; 057import org.openstreetmap.josm.tools.ColorScale; 058import org.openstreetmap.josm.tools.JosmRuntimeException; 059import org.openstreetmap.josm.tools.Logging; 060import org.openstreetmap.josm.tools.Stopwatch; 061import org.openstreetmap.josm.tools.Utils; 062 063/** 064 * Class that helps to draw large set of GPS tracks with different colors and options 065 * @since 7319 066 */ 067public class GpxDrawHelper implements SoMChangeListener, MapViewPaintable.LayerPainter, PaintableInvalidationListener, GpxDataChangeListener { 068 069 /** 070 * The default color property that is used for drawing GPX points. 071 * @since 15496 072 */ 073 public static final NamedColorProperty DEFAULT_COLOR_PROPERTY = new NamedColorProperty(marktr("gps point"), Color.magenta); 074 075 private final GpxData data; 076 private final GpxLayer layer; 077 078 // draw lines between points belonging to different segments 079 private boolean forceLines; 080 // use alpha blending for line draw 081 private boolean alphaLines; 082 // draw direction arrows on the lines 083 private boolean arrows; 084 /** width of line for paint **/ 085 private int lineWidth; 086 /** don't draw lines if longer than x meters **/ 087 private int maxLineLength; 088 // draw lines 089 private boolean lines; 090 /** paint large dots for points **/ 091 private boolean large; 092 private int largesize; 093 private boolean hdopCircle; 094 /** paint direction arrow with alternate math. may be faster **/ 095 private boolean arrowsFast; 096 /** don't draw arrows nearer to each other than this **/ 097 private int arrowsDelta; 098 private double minTrackDurationForTimeColoring; 099 100 /** maximum value of displayed HDOP, minimum is 0 */ 101 private int hdoprange; 102 103 private static final double PHI = Utils.toRadians(15); 104 105 //// Variables used only to check cache validity 106 private boolean computeCacheInSync; 107 private int computeCacheMaxLineLengthUsed; 108 private Color computeCacheColorUsed; 109 private boolean computeCacheColorDynamic; 110 private ColorMode computeCacheColored; 111 private int computeCacheVelocityTune; 112 private int computeCacheHeatMapDrawColorTableIdx; 113 private boolean computeCacheHeatMapDrawPointMode; 114 private int computeCacheHeatMapDrawGain; 115 private int computeCacheHeatMapDrawLowerLimit; 116 117 private Color colorCache; 118 private Color colorCacheTransparent; 119 120 //// Color-related fields 121 /** Mode of the line coloring **/ 122 private ColorMode colored; 123 /** max speed for coloring - allows to tweak line coloring for different speed levels. **/ 124 private int velocityTune; 125 private boolean colorModeDynamic; 126 private Color neutralColor; 127 private int largePointAlpha; 128 129 // default access is used to allow changing from plugins 130 private ColorScale velocityScale; 131 /** Colors (without custom alpha channel, if given) for HDOP painting. **/ 132 private ColorScale hdopScale; 133 private ColorScale qualityScale; 134 private ColorScale dateScale; 135 private ColorScale directionScale; 136 137 /** Opacity for hdop points **/ 138 private int hdopAlpha; 139 140 // lookup array to draw arrows without doing any math 141 private static final int ll0 = 9; 142 private static final int sl4 = 5; 143 private static final int sl9 = 3; 144 private static final int[][] dir = { 145 {+sl4, +ll0, +ll0, +sl4}, {-sl9, +ll0, +sl9, +ll0}, 146 {-ll0, +sl4, -sl4, +ll0}, {-ll0, -sl9, -ll0, +sl9}, 147 {-sl4, -ll0, -ll0, -sl4}, {+sl9, -ll0, -sl9, -ll0}, 148 {+ll0, -sl4, +sl4, -ll0}, {+ll0, +sl9, +ll0, -sl9} 149 }; 150 151 /** heat map parameters **/ 152 153 // draw small extra line 154 private boolean heatMapDrawExtraLine; 155 // used index for color table (parameter) 156 private int heatMapDrawColorTableIdx; 157 // use point or line draw mode 158 private boolean heatMapDrawPointMode; 159 // extra gain > 0 or < 0 attenuation, 0 = default 160 private int heatMapDrawGain; 161 // do not draw elements with value lower than this limit 162 private int heatMapDrawLowerLimit; 163 164 // normal buffered image and draw object (cached) 165 private BufferedImage heatMapImgGray; 166 private Graphics2D heatMapGraph2d; 167 168 // some cached values 169 Rectangle heatMapCacheScreenBounds = new Rectangle(); 170 MapViewState heatMapMapViewState; 171 int heatMapCacheLineWith; 172 173 // copied value for line drawing 174 private final List<Integer> heatMapPolyX = new ArrayList<>(); 175 private final List<Integer> heatMapPolyY = new ArrayList<>(); 176 177 // setup color maps used by heat map 178 private static Color[] heatMapLutColorJosmInferno = createColorFromResource("inferno"); 179 private static Color[] heatMapLutColorJosmViridis = createColorFromResource("viridis"); 180 private static Color[] heatMapLutColorJosmBrown2Green = createColorFromResource("brown2green"); 181 private static Color[] heatMapLutColorJosmRed2Blue = createColorFromResource("red2blue"); 182 183 private static Color[] rtkLibQualityColors = { 184 Color.GREEN, // Fixed, solution by carrier‐based relative positioning and the integer ambiguity is properly resolved. 185 Color.ORANGE, // Float, solution by carrier‐based relative positioning but the integer ambiguity is not resolved. 186 Color.PINK, // Reserved 187 Color.BLUE, // DGPS, solution by code‐based DGPS solutions or single point positioning with SBAS corrections 188 Color.RED, // Single, solution by single point positioning 189 Color.CYAN // PPP 190 }; 191 192 // user defined heatmap color 193 private Color[] heatMapLutColor = createColorLut(0, Color.BLACK, Color.WHITE); 194 195 // The heat map was invalidated since the last draw. 196 private boolean gpxLayerInvalidated; 197 198 private void setupColors() { 199 hdopAlpha = Config.getPref().getInt("hdop.color.alpha", -1); 200 velocityScale = ColorScale.createHSBScale(256); 201 /** Colors (without custom alpha channel, if given) for HDOP painting. **/ 202 hdopScale = ColorScale.createHSBScale(256).makeReversed().addTitle(tr("HDOP")); 203 qualityScale = ColorScale.createFixedScale(rtkLibQualityColors).addTitle(tr("Quality")); 204 dateScale = ColorScale.createHSBScale(256).addTitle(tr("Time")); 205 directionScale = ColorScale.createCyclicScale(256).setIntervalCount(4).addTitle(tr("Direction")); 206 207 systemOfMeasurementChanged(null, null); 208 } 209 210 @Override 211 public void systemOfMeasurementChanged(String oldSoM, String newSoM) { 212 SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement(); 213 velocityScale.addTitle(tr("Velocity, {0}", som.speedName)); 214 layer.invalidate(); 215 } 216 217 /** 218 * Different color modes 219 */ 220 public enum ColorMode { 221 /** 222 * No special colors 223 */ 224 NONE, 225 /** 226 * Color by velocity 227 */ 228 VELOCITY, 229 /** 230 * Color by accuracy 231 */ 232 HDOP, 233 /** 234 * Color by traveling direction 235 */ 236 DIRECTION, 237 /** 238 * Color by time 239 */ 240 TIME, 241 /** 242 * Color using a heatmap instead of normal lines 243 */ 244 HEATMAP, 245 /** 246 * Color by quality (RTKLib) 247 */ 248 QUALITY; 249 250 static ColorMode fromIndex(final int index) { 251 return values()[index]; 252 } 253 254 int toIndex() { 255 return Arrays.asList(values()).indexOf(this); 256 } 257 } 258 259 /** 260 * Constructs a new {@code GpxDrawHelper}. 261 * @param gpxLayer The layer to draw 262 * @since 12157 263 */ 264 public GpxDrawHelper(GpxLayer gpxLayer) { 265 layer = gpxLayer; 266 data = gpxLayer.data; 267 data.addChangeListener(this); 268 269 layer.addInvalidationListener(this); 270 SystemOfMeasurement.addSoMChangeListener(this); 271 setupColors(); 272 } 273 274 /** 275 * Read coloring mode for specified layer from preferences 276 * @return coloring mode 277 */ 278 public ColorMode getColorMode() { 279 try { 280 int i = optInt("colormode"); 281 if (i == -1) i = 0; //global 282 return ColorMode.fromIndex(i); 283 } catch (IndexOutOfBoundsException e) { 284 Logging.warn(e); 285 } 286 return ColorMode.NONE; 287 } 288 289 private String opt(String key) { 290 return GPXSettingsPanel.getLayerPref(layer, key); 291 } 292 293 private boolean optBool(String key) { 294 return Boolean.parseBoolean(opt(key)); 295 } 296 297 private int optInt(String key) { 298 return GPXSettingsPanel.getLayerPrefInt(layer, key); 299 } 300 301 /** 302 * Read all drawing-related settings from preferences 303 **/ 304 public void readPreferences() { 305 forceLines = optBool("lines.force"); 306 arrows = optBool("lines.arrows"); 307 arrowsFast = optBool("lines.arrows.fast"); 308 arrowsDelta = optInt("lines.arrows.min-distance"); 309 lineWidth = optInt("lines.width"); 310 alphaLines = optBool("lines.alpha-blend"); 311 312 int l = optInt("lines"); 313 // -1 = global (default: all) 314 // 0 = none 315 // 1 = local 316 // 2 = all 317 if (!data.fromServer) { //local settings apply 318 maxLineLength = optInt("lines.max-length.local"); 319 lines = l != 0; // don't draw if "none" 320 } else { 321 maxLineLength = optInt("lines.max-length"); 322 lines = l != 0 && l != 1; //don't draw if "none" or "local only" 323 } 324 large = optBool("points.large"); 325 largesize = optInt("points.large.size"); 326 hdopCircle = optBool("points.hdopcircle"); 327 colored = getColorMode(); 328 velocityTune = optInt("colormode.velocity.tune"); 329 colorModeDynamic = optBool("colormode.dynamic-range"); 330 /* good HDOP's are between 1 and 3, very bad HDOP's go into 3 digit values */ 331 hdoprange = Config.getPref().getInt("hdop.range", 7); 332 minTrackDurationForTimeColoring = optInt("colormode.time.min-distance"); 333 largePointAlpha = optInt("points.large.alpha") & 0xFF; 334 335 // get heatmap parameters 336 heatMapDrawExtraLine = optBool("colormode.heatmap.line-extra"); 337 heatMapDrawColorTableIdx = optInt("colormode.heatmap.colormap"); 338 heatMapDrawPointMode = optBool("colormode.heatmap.use-points"); 339 heatMapDrawGain = optInt("colormode.heatmap.gain"); 340 heatMapDrawLowerLimit = optInt("colormode.heatmap.lower-limit"); 341 342 // shrink to range 343 heatMapDrawGain = Utils.clamp(heatMapDrawGain, -10, 10); 344 neutralColor = DEFAULT_COLOR_PROPERTY.get(); 345 velocityScale.setNoDataColor(neutralColor); 346 dateScale.setNoDataColor(neutralColor); 347 hdopScale.setNoDataColor(neutralColor); 348 qualityScale.setNoDataColor(neutralColor); 349 directionScale.setNoDataColor(neutralColor); 350 351 largesize += lineWidth; 352 } 353 354 @Override 355 public void paint(MapViewGraphics graphics) { 356 Bounds clipBounds = graphics.getClipBounds().getLatLonBoundsBox(); 357 List<WayPoint> visibleSegments = listVisibleSegments(clipBounds); 358 if (!visibleSegments.isEmpty()) { 359 readPreferences(); 360 drawAll(graphics.getDefaultGraphics(), graphics.getMapView(), visibleSegments, clipBounds); 361 if (graphics.getMapView().getLayerManager().getActiveLayer() == layer) { 362 drawColorBar(graphics.getDefaultGraphics(), graphics.getMapView()); 363 } 364 } 365 } 366 367 private List<WayPoint> listVisibleSegments(Bounds box) { 368 WayPoint last = null; 369 LinkedList<WayPoint> visibleSegments = new LinkedList<>(); 370 371 ensureTrackVisibilityLength(); 372 for (Line segment : data.getLinesIterable(layer.trackVisibility)) { 373 374 for (WayPoint pt : segment) { 375 Bounds b = new Bounds(pt.getCoor()); 376 if (pt.drawLine && last != null) { 377 b.extend(last.getCoor()); 378 } 379 if (b.intersects(box)) { 380 if (last != null && (visibleSegments.isEmpty() 381 || visibleSegments.getLast() != last)) { 382 if (last.drawLine) { 383 WayPoint l = new WayPoint(last); 384 l.drawLine = false; 385 visibleSegments.add(l); 386 } else { 387 visibleSegments.add(last); 388 } 389 } 390 visibleSegments.add(pt); 391 } 392 last = pt; 393 } 394 } 395 return visibleSegments; 396 } 397 398 /** ensures the trackVisibility array has the correct length without losing data. 399 * TODO: Make this nicer by syncing the trackVisibility automatically. 400 * additional entries are initialized to true; 401 */ 402 private void ensureTrackVisibilityLength() { 403 final int l = data.getTracks().size(); 404 if (l == layer.trackVisibility.length) 405 return; 406 final int m = Math.min(l, layer.trackVisibility.length); 407 layer.trackVisibility = Arrays.copyOf(layer.trackVisibility, l); 408 for (int i = m; i < l; i++) { 409 layer.trackVisibility[i] = true; 410 } 411 } 412 413 /** 414 * Draw all enabled GPX elements of layer. 415 * @param g the common draw object to use 416 * @param mv the meta data to current displayed area 417 * @param visibleSegments segments visible in the current scope of mv 418 * @param clipBounds the clipping rectangle for the current view 419 * @since 14748 : new parameter clipBounds 420 */ 421 public void drawAll(Graphics2D g, MapView mv, List<WayPoint> visibleSegments, Bounds clipBounds) { 422 423 final Stopwatch stopwatch = Stopwatch.createStarted(); 424 425 checkCache(); 426 427 // STEP 2b - RE-COMPUTE CACHE DATA ********************* 428 if (!computeCacheInSync) { // don't compute if the cache is good 429 calculateColors(); 430 // update the WaiPoint.drawline attributes 431 visibleSegments.clear(); 432 visibleSegments.addAll(listVisibleSegments(clipBounds)); 433 } 434 435 fixColors(visibleSegments); 436 437 // backup the environment 438 Composite oldComposite = g.getComposite(); 439 Stroke oldStroke = g.getStroke(); 440 Paint oldPaint = g.getPaint(); 441 442 // set hints for the render 443 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 444 Config.getPref().getBoolean("mappaint.gpx.use-antialiasing", false) ? 445 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 446 447 if (lineWidth > 0) { 448 g.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); 449 } 450 451 // global enabled or select via color 452 boolean useHeatMap = ColorMode.HEATMAP == colored; 453 454 // default global alpha level 455 float layerAlpha = 1.00f; 456 457 // extract current alpha blending value 458 if (oldComposite instanceof AlphaComposite) { 459 layerAlpha = ((AlphaComposite) oldComposite).getAlpha(); 460 } 461 462 // use heatmap background layer 463 if (useHeatMap) { 464 drawHeatMap(g, mv, visibleSegments); 465 } else { 466 // use normal line style or alpha-blending lines 467 if (!alphaLines) { 468 drawLines(g, mv, visibleSegments); 469 } else { 470 drawLinesAlpha(g, mv, visibleSegments, layerAlpha); 471 } 472 } 473 474 // override global alpha settings (smooth overlay) 475 if (alphaLines || useHeatMap) { 476 g.setComposite(AlphaComposite.SrcOver.derive(0.25f * layerAlpha)); 477 } 478 479 // normal overlays 480 drawArrows(g, mv, visibleSegments); 481 drawPoints(g, mv, visibleSegments); 482 483 // restore environment 484 g.setPaint(oldPaint); 485 g.setStroke(oldStroke); 486 g.setComposite(oldComposite); 487 488 // show some debug info 489 if (Logging.isDebugEnabled() && !visibleSegments.isEmpty()) { 490 Logging.debug("gpxdraw::draw takes " + 491 stopwatch + 492 "(" + 493 "segments= " + visibleSegments.size() + 494 ", per 10000 = " + Utils.getDurationString(10_000 * stopwatch.elapsed() / visibleSegments.size()) + 495 ")" 496 ); 497 } 498 } 499 500 /** 501 * Calculate colors of way segments based on latest configuration settings 502 */ 503 public void calculateColors() { 504 double minval = +1e10; 505 double maxval = -1e10; 506 WayPoint oldWp = null; 507 508 if (colorModeDynamic) { 509 if (colored == ColorMode.VELOCITY) { 510 final List<Double> velocities = new ArrayList<>(); 511 for (Line segment : data.getLinesIterable(null)) { 512 if (!forceLines) { 513 oldWp = null; 514 } 515 for (WayPoint trkPnt : segment) { 516 if (!trkPnt.isLatLonKnown()) { 517 continue; 518 } 519 if (oldWp != null && trkPnt.getTimeInMillis() > oldWp.getTimeInMillis()) { 520 double vel = trkPnt.getCoor().greatCircleDistance(oldWp.getCoor()) 521 / (trkPnt.getTime() - oldWp.getTime()); 522 velocities.add(vel); 523 } 524 oldWp = trkPnt; 525 } 526 } 527 Collections.sort(velocities); 528 if (velocities.isEmpty()) { 529 velocityScale.setRange(0, 120/3.6); 530 } else { 531 minval = velocities.get(velocities.size() / 20); // 5% percentile to remove outliers 532 maxval = velocities.get(velocities.size() * 19 / 20); // 95% percentile to remove outliers 533 velocityScale.setRange(minval, maxval); 534 } 535 } else if (colored == ColorMode.HDOP) { 536 for (Line segment : data.getLinesIterable(null)) { 537 for (WayPoint trkPnt : segment) { 538 Object val = trkPnt.get(GpxConstants.PT_HDOP); 539 if (val != null) { 540 double hdop = ((Float) val).doubleValue(); 541 if (hdop > maxval) { 542 maxval = hdop; 543 } 544 if (hdop < minval) { 545 minval = hdop; 546 } 547 } 548 } 549 } 550 if (minval >= maxval) { 551 hdopScale.setRange(0, 100); 552 } else { 553 hdopScale.setRange(minval, maxval); 554 } 555 } 556 oldWp = null; 557 } else { // color mode not dynamic 558 velocityScale.setRange(0, velocityTune); 559 hdopScale.setRange(0, hdoprange); 560 qualityScale.setRange(1, rtkLibQualityColors.length); 561 } 562 double now = System.currentTimeMillis()/1000.0; 563 if (colored == ColorMode.TIME) { 564 Date[] bounds = data.getMinMaxTimeForAllTracks(); 565 if (bounds.length >= 2) { 566 minval = bounds[0].getTime()/1000.0; 567 maxval = bounds[1].getTime()/1000.0; 568 } else { 569 minval = 0; 570 maxval = now; 571 } 572 dateScale.setRange(minval, maxval); 573 } 574 575 // Now the colors for all the points will be assigned 576 for (Line segment : data.getLinesIterable(null)) { 577 if (!forceLines) { // don't draw lines between segments, unless forced to 578 oldWp = null; 579 } 580 for (WayPoint trkPnt : segment) { 581 LatLon c = trkPnt.getCoor(); 582 trkPnt.customColoring = segment.getColor(); 583 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 584 continue; 585 } 586 // now we are sure some color will be assigned 587 Color color = null; 588 589 if (colored == ColorMode.HDOP) { 590 color = hdopScale.getColor((Float) trkPnt.get(GpxConstants.PT_HDOP)); 591 } else if (colored == ColorMode.QUALITY) { 592 color = qualityScale.getColor((Integer) trkPnt.get(GpxConstants.RTKLIB_Q)); 593 } 594 if (oldWp != null) { // other coloring modes need segment for calcuation 595 double dist = c.greatCircleDistance(oldWp.getCoor()); 596 boolean noDraw = false; 597 switch (colored) { 598 case VELOCITY: 599 double dtime = trkPnt.getTime() - oldWp.getTime(); 600 if (dtime > 0) { 601 color = velocityScale.getColor(dist / dtime); 602 } else { 603 color = velocityScale.getNoDataColor(); 604 } 605 break; 606 case DIRECTION: 607 double dirColor = oldWp.getCoor().bearing(trkPnt.getCoor()); 608 color = directionScale.getColor(dirColor); 609 break; 610 case TIME: 611 double t = trkPnt.getTime(); 612 // skip bad timestamps and very short tracks 613 if (t > 0 && t <= now && maxval - minval > minTrackDurationForTimeColoring) { 614 color = dateScale.getColor(t); 615 } else { 616 color = dateScale.getNoDataColor(); 617 } 618 break; 619 default: // Do nothing 620 } 621 if (!noDraw && (!segment.isUnordered() || !data.fromServer) && (maxLineLength == -1 || dist <= maxLineLength)) { 622 trkPnt.drawLine = true; 623 double bearing = oldWp.getCoor().bearing(trkPnt.getCoor()); 624 trkPnt.dir = ((int) (bearing / Math.PI * 4 + 1.5)) % 8; 625 } else { 626 trkPnt.drawLine = false; 627 } 628 } else { // make sure we reset outdated data 629 trkPnt.drawLine = false; 630 color = segment.getColor(); 631 } 632 if (color != null) { 633 trkPnt.customColoring = color; 634 } 635 oldWp = trkPnt; 636 } 637 } 638 639 // heat mode 640 if (ColorMode.HEATMAP == colored) { 641 642 // get new user color map and refresh visibility level 643 heatMapLutColor = createColorLut(heatMapDrawLowerLimit, 644 selectColorMap(neutralColor != null ? neutralColor : Color.WHITE, heatMapDrawColorTableIdx)); 645 646 // force redraw of image 647 heatMapMapViewState = null; 648 } 649 650 computeCacheInSync = true; 651 } 652 653 /** 654 * Draw all GPX ways segments 655 * @param g the common draw object to use 656 * @param mv the meta data to current displayed area 657 * @param visibleSegments segments visible in the current scope of mv 658 */ 659 private void drawLines(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) { 660 if (lines) { 661 Point old = null; 662 for (WayPoint trkPnt : visibleSegments) { 663 if (!trkPnt.isLatLonKnown()) { 664 old = null; 665 continue; 666 } 667 Point screen = mv.getPoint(trkPnt); 668 // skip points that are on the same screenposition 669 if (trkPnt.drawLine && old != null && ((old.x != screen.x) || (old.y != screen.y))) { 670 g.setColor(trkPnt.customColoring); 671 g.drawLine(old.x, old.y, screen.x, screen.y); 672 } 673 old = screen; 674 } 675 } 676 } 677 678 /** 679 * Draw all GPX arrays 680 * @param g the common draw object to use 681 * @param mv the meta data to current displayed area 682 * @param visibleSegments segments visible in the current scope of mv 683 */ 684 private void drawArrows(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) { 685 /**************************************************************** 686 ********** STEP 3b - DRAW NICE ARROWS ************************** 687 ****************************************************************/ 688 if (lines && arrows && !arrowsFast) { 689 Point old = null; 690 Point oldA = null; // last arrow painted 691 for (WayPoint trkPnt : visibleSegments) { 692 if (!trkPnt.isLatLonKnown()) { 693 old = null; 694 continue; 695 } 696 if (trkPnt.drawLine) { 697 Point screen = mv.getPoint(trkPnt); 698 // skip points that are on the same screenposition 699 if (old != null 700 && (oldA == null || screen.x < oldA.x - arrowsDelta || screen.x > oldA.x + arrowsDelta 701 || screen.y < oldA.y - arrowsDelta || screen.y > oldA.y + arrowsDelta)) { 702 g.setColor(trkPnt.customColoring); 703 double t = Math.atan2((double) screen.y - old.y, (double) screen.x - old.x) + Math.PI; 704 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)), 705 (int) (screen.y + 10 * Math.sin(t - PHI))); 706 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)), 707 (int) (screen.y + 10 * Math.sin(t + PHI))); 708 oldA = screen; 709 } 710 old = screen; 711 } 712 } // end for trkpnt 713 } 714 715 /**************************************************************** 716 ********** STEP 3c - DRAW FAST ARROWS ************************** 717 ****************************************************************/ 718 if (lines && arrows && arrowsFast) { 719 Point old = null; 720 Point oldA = null; // last arrow painted 721 for (WayPoint trkPnt : visibleSegments) { 722 LatLon c = trkPnt.getCoor(); 723 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 724 continue; 725 } 726 if (trkPnt.drawLine) { 727 Point screen = mv.getPoint(trkPnt); 728 // skip points that are on the same screenposition 729 if (old != null 730 && (oldA == null || screen.x < oldA.x - arrowsDelta || screen.x > oldA.x + arrowsDelta 731 || screen.y < oldA.y - arrowsDelta || screen.y > oldA.y + arrowsDelta)) { 732 g.setColor(trkPnt.customColoring); 733 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y 734 + dir[trkPnt.dir][1]); 735 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y 736 + dir[trkPnt.dir][3]); 737 oldA = screen; 738 } 739 old = screen; 740 } 741 } // end for trkpnt 742 } 743 } 744 745 /** 746 * Draw all GPX points 747 * @param g the common draw object to use 748 * @param mv the meta data to current displayed area 749 * @param visibleSegments segments visible in the current scope of mv 750 */ 751 private void drawPoints(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) { 752 /**************************************************************** 753 ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE ********* 754 ****************************************************************/ 755 if (large || hdopCircle) { 756 final int halfSize = largesize/2; 757 for (WayPoint trkPnt : visibleSegments) { 758 LatLon c = trkPnt.getCoor(); 759 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 760 continue; 761 } 762 Point screen = mv.getPoint(trkPnt); 763 764 if (hdopCircle && trkPnt.get(GpxConstants.PT_HDOP) != null) { 765 // hdop value 766 float hdop = (Float) trkPnt.get(GpxConstants.PT_HDOP); 767 if (hdop < 0) { 768 hdop = 0; 769 } 770 Color customColoringTransparent = hdopAlpha < 0 ? trkPnt.customColoring : 771 new Color((trkPnt.customColoring.getRGB() & 0x00ffffff) | (hdopAlpha << 24), true); 772 g.setColor(customColoringTransparent); 773 // hdop circles 774 int hdopp = mv.getPoint(new LatLon( 775 trkPnt.getCoor().lat(), 776 trkPnt.getCoor().lon() + 2d*6*hdop*360/40000000d)).x - screen.x; 777 g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360); 778 } 779 if (large) { 780 // color the large GPS points like the gps lines 781 if (trkPnt.customColoring != null) { 782 if (trkPnt.customColoring.equals(colorCache) && colorCacheTransparent != null) { 783 g.setColor(colorCacheTransparent); 784 } else { 785 Color customColoringTransparent = largePointAlpha < 0 ? trkPnt.customColoring : 786 new Color((trkPnt.customColoring.getRGB() & 0x00ffffff) | (largePointAlpha << 24), true); 787 788 g.setColor(customColoringTransparent); 789 colorCache = trkPnt.customColoring; 790 colorCacheTransparent = customColoringTransparent; 791 } 792 } 793 g.fillRect(screen.x-halfSize, screen.y-halfSize, largesize, largesize); 794 } 795 } // end for trkpnt 796 } // end if large || hdopcircle 797 798 /**************************************************************** 799 ********** STEP 3e - DRAW SMALL POINTS FOR LINES *************** 800 ****************************************************************/ 801 if (!large && lines) { 802 g.setColor(neutralColor); 803 for (WayPoint trkPnt : visibleSegments) { 804 LatLon c = trkPnt.getCoor(); 805 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 806 continue; 807 } 808 if (!trkPnt.drawLine) { 809 g.setColor(trkPnt.customColoring); 810 Point screen = mv.getPoint(trkPnt); 811 g.drawRect(screen.x, screen.y, 0, 0); 812 } 813 } // end for trkpnt 814 } // end if large 815 816 /**************************************************************** 817 ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ******** 818 ****************************************************************/ 819 if (!large && !lines) { 820 g.setColor(neutralColor); 821 for (WayPoint trkPnt : visibleSegments) { 822 LatLon c = trkPnt.getCoor(); 823 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 824 continue; 825 } 826 Point screen = mv.getPoint(trkPnt); 827 g.setColor(trkPnt.customColoring); 828 g.drawRect(screen.x, screen.y, 0, 0); 829 } // end for trkpnt 830 } // end if large 831 } 832 833 /** 834 * Draw GPX lines by using alpha blending 835 * @param g the common draw object to use 836 * @param mv the meta data to current displayed area 837 * @param visibleSegments segments visible in the current scope of mv 838 * @param layerAlpha the color alpha value set for that operation 839 */ 840 private void drawLinesAlpha(Graphics2D g, MapView mv, List<WayPoint> visibleSegments, float layerAlpha) { 841 842 // 1st. backup the paint environment ---------------------------------- 843 Composite oldComposite = g.getComposite(); 844 Stroke oldStroke = g.getStroke(); 845 Paint oldPaint = g.getPaint(); 846 847 // 2nd. determine current scale factors ------------------------------- 848 849 // adjust global settings 850 final int globalLineWidth = Utils.clamp(lineWidth, 1, 20); 851 852 // cache scale of view 853 final double zoomScale = mv.getDist100Pixel() / 50.0f; 854 855 // 3rd. determine current paint parameters ----------------------------- 856 857 // alpha value is based on zoom and line with combined with global layer alpha 858 float theLineAlpha = (float) Utils.clamp((0.50 / zoomScale) / (globalLineWidth + 1), 0.01, 0.50) * layerAlpha; 859 final int theLineWith = (int) (lineWidth / zoomScale) + 1; 860 861 // 4th setup virtual paint area ---------------------------------------- 862 863 // set line format and alpha channel for all overlays (more lines -> few overlap -> more transparency) 864 g.setStroke(new BasicStroke(theLineWith, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); 865 g.setComposite(AlphaComposite.SrcOver.derive(theLineAlpha)); 866 867 // last used / calculated entries 868 Point lastPaintPnt = null; 869 870 // 5th draw the layer --------------------------------------------------- 871 872 // for all points 873 for (WayPoint trkPnt : visibleSegments) { 874 875 // transform coordinates 876 final Point paintPnt = mv.getPoint(trkPnt); 877 878 // skip single points 879 if (lastPaintPnt != null && trkPnt.drawLine && !lastPaintPnt.equals(paintPnt)) { 880 881 // set different color 882 g.setColor(trkPnt.customColoring); 883 884 // draw it 885 g.drawLine(lastPaintPnt.x, lastPaintPnt.y, paintPnt.x, paintPnt.y); 886 } 887 888 lastPaintPnt = paintPnt; 889 } 890 891 // @last restore modified paint environment ----------------------------- 892 g.setPaint(oldPaint); 893 g.setStroke(oldStroke); 894 g.setComposite(oldComposite); 895 } 896 897 /** 898 * Generates a linear gradient map image 899 * 900 * @param width image width 901 * @param height image height 902 * @param colors 1..n color descriptions 903 * @return image object 904 */ 905 protected static BufferedImage createImageGradientMap(int width, int height, Color... colors) { 906 907 // create image an paint object 908 final BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 909 final Graphics2D g = img.createGraphics(); 910 911 float[] fract = new float[ colors.length ]; 912 913 // distribute fractions (define position of color in map) 914 for (int i = 0; i < colors.length; ++i) { 915 fract[i] = i * (1.0f / colors.length); 916 } 917 918 // draw the gradient map 919 LinearGradientPaint gradient = new LinearGradientPaint(0, 0, width, height, fract, colors, 920 MultipleGradientPaint.CycleMethod.NO_CYCLE); 921 g.setPaint(gradient); 922 g.fillRect(0, 0, width, height); 923 g.dispose(); 924 925 // access it via raw interface 926 return img; 927 } 928 929 /** 930 * Creates a distributed colormap by linear blending between colors 931 * @param lowerLimit lower limit for first visible color 932 * @param colors 1..n colors 933 * @return array of Color objects 934 */ 935 protected static Color[] createColorLut(int lowerLimit, Color... colors) { 936 937 // number of lookup entries 938 final int tableSize = 256; 939 940 // access it via raw interface 941 final Raster imgRaster = createImageGradientMap(tableSize, 1, colors).getData(); 942 943 // the pixel storage 944 int[] pixel = new int[1]; 945 946 Color[] colorTable = new Color[tableSize]; 947 948 // map the range 0..255 to 0..pi/2 949 final double mapTo90Deg = Math.PI / 2.0 / 255.0; 950 951 // create the lookup table 952 for (int i = 0; i < tableSize; i++) { 953 954 // get next single pixel 955 imgRaster.getDataElements(i, 0, pixel); 956 957 // get color and map 958 Color c = new Color(pixel[0]); 959 960 // smooth alpha like sin curve 961 int alpha = (i > lowerLimit) ? (int) (Math.sin((i-lowerLimit) * mapTo90Deg) * 255) : 0; 962 963 // alpha with pre-offset, first color -> full transparent 964 alpha = alpha > 0 ? (20 + alpha) : 0; 965 966 // shrink to maximum bound 967 if (alpha > 255) { 968 alpha = 255; 969 } 970 971 // increase transparency for higher values ( avoid big saturation ) 972 if (i > 240 && 255 == alpha) { 973 alpha -= (i - 240); 974 } 975 976 // fill entry in table, assign a alpha value 977 colorTable[i] = new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha); 978 } 979 980 // transform into lookup table 981 return colorTable; 982 } 983 984 /** 985 * Creates a darker color 986 * @param in Color object 987 * @param adjust darker adjustment amount 988 * @return new Color 989 */ 990 protected static Color darkerColor(Color in, float adjust) { 991 992 final float r = (float) in.getRed()/255; 993 final float g = (float) in.getGreen()/255; 994 final float b = (float) in.getBlue()/255; 995 996 return new Color(r*adjust, g*adjust, b*adjust); 997 } 998 999 /** 1000 * Creates a colormap by using a static color map with 1..n colors (RGB 0.0 ..1.0) 1001 * @param str the filename (without extension) to look for into data/gpx 1002 * @return the parsed colormap 1003 */ 1004 protected static Color[] createColorFromResource(String str) { 1005 1006 // create resource string 1007 final String colorFile = "resource://data/gpx/" + str + ".txt"; 1008 1009 List<Color> colorList = new ArrayList<>(); 1010 1011 // try to load the file 1012 try (CachedFile cf = new CachedFile(colorFile); BufferedReader br = cf.getContentReader()) { 1013 1014 String line; 1015 1016 // process lines 1017 while ((line = br.readLine()) != null) { 1018 1019 // use comma as separator 1020 String[] column = line.split(","); 1021 1022 // empty or comment line 1023 if (column.length < 3 || column[0].startsWith("#")) { 1024 continue; 1025 } 1026 1027 // extract RGB value 1028 float r = Float.parseFloat(column[0]); 1029 float g = Float.parseFloat(column[1]); 1030 float b = Float.parseFloat(column[2]); 1031 1032 // some color tables are 0..1.0 and some 0.255 1033 float scale = (r < 1 && g < 1 && b < 1) ? 1 : 255; 1034 1035 colorList.add(new Color(r/scale, g/scale, b/scale)); 1036 } 1037 } catch (IOException e) { 1038 throw new JosmRuntimeException(e); 1039 } 1040 1041 // fallback if empty or failed 1042 if (colorList.isEmpty()) { 1043 colorList.add(Color.BLACK); 1044 colorList.add(Color.WHITE); 1045 } else { 1046 // add additional darker elements to end of list 1047 final Color lastColor = colorList.get(colorList.size() - 1); 1048 colorList.add(darkerColor(lastColor, 0.975f)); 1049 colorList.add(darkerColor(lastColor, 0.950f)); 1050 } 1051 1052 return createColorLut(0, colorList.toArray(new Color[0])); 1053 } 1054 1055 /** 1056 * Returns the next user color map 1057 * 1058 * @param userColor - default or fallback user color 1059 * @param tableIdx - selected user color index 1060 * @return color array 1061 */ 1062 protected static Color[] selectColorMap(Color userColor, int tableIdx) { 1063 1064 // generate new user color map ( dark, user color, white ) 1065 Color[] userColor1 = createColorLut(0, userColor.darker(), userColor, userColor.brighter(), Color.WHITE); 1066 1067 // generate new user color map ( white -> color ) 1068 Color[] userColor2 = createColorLut(0, Color.WHITE, Color.WHITE, userColor); 1069 1070 // generate new user color map 1071 Color[] colorTrafficLights = createColorLut(0, Color.WHITE, Color.GREEN.darker(), Color.YELLOW, Color.RED); 1072 1073 // decide what, keep order is sync with setting on GUI 1074 Color[][] lut = { 1075 userColor1, 1076 userColor2, 1077 colorTrafficLights, 1078 heatMapLutColorJosmInferno, 1079 heatMapLutColorJosmViridis, 1080 heatMapLutColorJosmBrown2Green, 1081 heatMapLutColorJosmRed2Blue 1082 }; 1083 1084 // default case 1085 Color[] nextUserColor = userColor1; 1086 1087 // select by index 1088 if (tableIdx < lut.length) { 1089 nextUserColor = lut[ tableIdx ]; 1090 } 1091 1092 // adjust color map 1093 return nextUserColor; 1094 } 1095 1096 /** 1097 * Generates a Icon 1098 * 1099 * @param userColor selected user color 1100 * @param tableIdx tabled index 1101 * @param size size of the image 1102 * @return a image icon that shows the 1103 */ 1104 public static ImageIcon getColorMapImageIcon(Color userColor, int tableIdx, int size) { 1105 return new ImageIcon(createImageGradientMap(size, size, selectColorMap(userColor, tableIdx))); 1106 } 1107 1108 /** 1109 * Draw gray heat map with current Graphics2D setting 1110 * @param gB the common draw object to use 1111 * @param mv the meta data to current displayed area 1112 * @param listSegm segments visible in the current scope of mv 1113 * @param foreComp composite use to draw foreground objects 1114 * @param foreStroke stroke use to draw foreground objects 1115 * @param backComp composite use to draw background objects 1116 * @param backStroke stroke use to draw background objects 1117 */ 1118 private void drawHeatGrayLineMap(Graphics2D gB, MapView mv, List<WayPoint> listSegm, 1119 Composite foreComp, Stroke foreStroke, 1120 Composite backComp, Stroke backStroke) { 1121 1122 // draw foreground 1123 boolean drawForeground = foreComp != null && foreStroke != null; 1124 1125 // set initial values 1126 gB.setStroke(backStroke); gB.setComposite(backComp); 1127 1128 // get last point in list 1129 final WayPoint lastPnt = !listSegm.isEmpty() ? listSegm.get(listSegm.size() - 1) : null; 1130 1131 // for all points, draw single lines by using optimized drawing 1132 for (WayPoint trkPnt : listSegm) { 1133 1134 // get transformed coordinates 1135 final Point paintPnt = mv.getPoint(trkPnt); 1136 1137 // end of line segment or end of list reached 1138 if (!trkPnt.drawLine || (lastPnt == trkPnt)) { 1139 1140 // convert to primitive type 1141 final int[] polyXArr = heatMapPolyX.stream().mapToInt(Integer::intValue).toArray(); 1142 final int[] polyYArr = heatMapPolyY.stream().mapToInt(Integer::intValue).toArray(); 1143 1144 // a.) draw background 1145 gB.drawPolyline(polyXArr, polyYArr, polyXArr.length); 1146 1147 // b.) draw extra foreground 1148 if (drawForeground && heatMapDrawExtraLine) { 1149 1150 gB.setStroke(foreStroke); gB.setComposite(foreComp); 1151 gB.drawPolyline(polyXArr, polyYArr, polyXArr.length); 1152 gB.setStroke(backStroke); gB.setComposite(backComp); 1153 } 1154 1155 // drop used points 1156 heatMapPolyX.clear(); heatMapPolyY.clear(); 1157 } 1158 1159 // store only the integer part (make sense because pixel is 1:1 here) 1160 heatMapPolyX.add((int) paintPnt.getX()); 1161 heatMapPolyY.add((int) paintPnt.getY()); 1162 } 1163 } 1164 1165 /** 1166 * Map the gray map to heat map and draw them with current Graphics2D setting 1167 * @param g the common draw object to use 1168 * @param imgGray gray scale input image 1169 * @param sampleRaster the line with for drawing 1170 * @param outlineWidth line width for outlines 1171 */ 1172 private void drawHeatMapGrayMap(Graphics2D g, BufferedImage imgGray, int sampleRaster, int outlineWidth) { 1173 1174 final int[] imgPixels = ((DataBufferInt) imgGray.getRaster().getDataBuffer()).getData(); 1175 1176 // samples offset and bounds are scaled with line width derived from zoom level 1177 final int offX = Math.max(1, sampleRaster); 1178 final int offY = Math.max(1, sampleRaster); 1179 1180 final int maxPixelX = imgGray.getWidth(); 1181 final int maxPixelY = imgGray.getHeight(); 1182 1183 // always full or outlines at big samples rasters 1184 final boolean drawOutlines = (outlineWidth > 0) && ((0 == sampleRaster) || (sampleRaster > 10)); 1185 1186 // backup stroke 1187 final Stroke oldStroke = g.getStroke(); 1188 1189 // use basic stroke for outlines and default transparency 1190 g.setStroke(new BasicStroke(outlineWidth)); 1191 1192 int lastPixelX = 0; 1193 int lastPixelColor = 0; 1194 1195 // resample gray scale image with line linear weight of next sample in line 1196 // process each line and draw pixels / rectangles with same color with one operations 1197 for (int y = 0; y < maxPixelY; y += offY) { 1198 1199 // the lines offsets 1200 final int lastLineOffset = maxPixelX * (y+0); 1201 final int nextLineOffset = maxPixelX * (y+1); 1202 1203 for (int x = 0; x < maxPixelX; x += offX) { 1204 1205 int thePixelColor = 0; int thePixelCount = 0; 1206 1207 // sample the image (it is gray scale) 1208 int offset = lastLineOffset + x; 1209 1210 // merge next pixels of window of line 1211 for (int k = 0; k < offX && (offset + k) < nextLineOffset; k++) { 1212 thePixelColor += imgPixels[offset+k] & 0xFF; 1213 thePixelCount++; 1214 } 1215 1216 // mean value 1217 thePixelColor = thePixelCount > 0 ? (thePixelColor / thePixelCount) : 0; 1218 1219 // restart -> use initial sample 1220 if (0 == x) { 1221 lastPixelX = 0; lastPixelColor = thePixelColor - 1; 1222 } 1223 1224 boolean bDrawIt = false; 1225 1226 // when one of segment is mapped to black 1227 bDrawIt = bDrawIt || (lastPixelColor == 0) || (thePixelColor == 0); 1228 1229 // different color 1230 bDrawIt = bDrawIt || (Math.abs(lastPixelColor-thePixelColor) > 0); 1231 1232 // when line is finished draw always 1233 bDrawIt = bDrawIt || (y >= (maxPixelY-offY)); 1234 1235 if (bDrawIt) { 1236 1237 // draw only foreground pixels 1238 if (lastPixelColor > 0) { 1239 1240 // gray to RGB mapping 1241 g.setColor(heatMapLutColor[ lastPixelColor ]); 1242 1243 // box from from last Y pixel to current pixel 1244 if (drawOutlines) { 1245 g.drawRect(lastPixelX, y, offX + x - lastPixelX, offY); 1246 } else { 1247 g.fillRect(lastPixelX, y, offX + x - lastPixelX, offY); 1248 } 1249 } 1250 1251 // restart detection 1252 lastPixelX = x; lastPixelColor = thePixelColor; 1253 } 1254 } 1255 } 1256 1257 // recover 1258 g.setStroke(oldStroke); 1259 } 1260 1261 /** 1262 * Collect and draw GPS segments and displays a heat-map 1263 * @param g the common draw object to use 1264 * @param mv the meta data to current displayed area 1265 * @param visibleSegments segments visible in the current scope of mv 1266 */ 1267 private void drawHeatMap(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) { 1268 1269 // get bounds of screen image and projection, zoom and adjust input parameters 1270 final Rectangle screenBounds = new Rectangle(mv.getWidth(), mv.getHeight()); 1271 final MapViewState mapViewState = mv.getState(); 1272 final double zoomScale = mv.getDist100Pixel() / 50.0f; 1273 1274 // adjust global settings ( zero = default line width ) 1275 final int globalLineWidth = (0 == lineWidth) ? 1 : Utils.clamp(lineWidth, 1, 20); 1276 1277 // 1st setup virtual paint area ---------------------------------------- 1278 1279 // new image buffer needed 1280 final boolean imageSetup = null == heatMapImgGray || !heatMapCacheScreenBounds.equals(screenBounds); 1281 1282 // screen bounds changed, need new image buffer ? 1283 if (imageSetup) { 1284 // we would use a "pure" grayscale image, but there is not efficient way to map gray scale values to RGB) 1285 heatMapImgGray = new BufferedImage(screenBounds.width, screenBounds.height, BufferedImage.TYPE_INT_ARGB); 1286 heatMapGraph2d = heatMapImgGray.createGraphics(); 1287 heatMapGraph2d.setBackground(new Color(0, 0, 0, 255)); 1288 heatMapGraph2d.setColor(Color.WHITE); 1289 1290 // fast draw ( maybe help or not ) 1291 heatMapGraph2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); 1292 heatMapGraph2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); 1293 heatMapGraph2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED); 1294 heatMapGraph2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); 1295 heatMapGraph2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); 1296 heatMapGraph2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); 1297 heatMapGraph2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED); 1298 1299 // cache it 1300 heatMapCacheScreenBounds = screenBounds; 1301 } 1302 1303 // 2nd. determine current scale factors ------------------------------- 1304 1305 // the line width (foreground: draw extra small footprint line of track) 1306 int lineWidthB = (int) Math.max(1.5f * (globalLineWidth / zoomScale) + 1, 2); 1307 int lineWidthF = lineWidthB > 2 ? (globalLineWidth - 1) : 0; 1308 1309 // global alpha adjustment 1310 float lineAlpha = (float) Utils.clamp((0.40 / zoomScale) / (globalLineWidth + 1), 0.01, 0.40); 1311 1312 // adjust 0.15 .. 1.85 1313 float scaleAlpha = 1.0f + ((heatMapDrawGain/10.0f) * 0.85f); 1314 1315 // add to calculated values 1316 float lineAlphaBPoint = (float) Utils.clamp((lineAlpha * 0.65) * scaleAlpha, 0.001, 0.90); 1317 float lineAlphaBLine = (float) Utils.clamp((lineAlpha * 1.00) * scaleAlpha, 0.001, 0.90); 1318 float lineAlphaFLine = (float) Utils.clamp((lineAlpha / 1.50) * scaleAlpha, 0.001, 0.90); 1319 1320 // 3rd Calculate the heat map data by draw GPX traces with alpha value ---------- 1321 1322 // recalculation of image needed 1323 final boolean imageRecalc = !mapViewState.equalsInWindow(heatMapMapViewState) 1324 || gpxLayerInvalidated 1325 || heatMapCacheLineWith != globalLineWidth; 1326 1327 // need re-generation of gray image ? 1328 if (imageSetup || imageRecalc) { 1329 1330 // clear background 1331 heatMapGraph2d.clearRect(0, 0, heatMapImgGray.getWidth(), heatMapImgGray.getHeight()); 1332 1333 // point or line blending 1334 if (heatMapDrawPointMode) { 1335 heatMapGraph2d.setComposite(AlphaComposite.SrcOver.derive(lineAlphaBPoint)); 1336 drawHeatGrayDotMap(heatMapGraph2d, mv, visibleSegments, lineWidthB); 1337 1338 } else { 1339 drawHeatGrayLineMap(heatMapGraph2d, mv, visibleSegments, 1340 lineWidthF > 1 ? AlphaComposite.SrcOver.derive(lineAlphaFLine) : null, 1341 new BasicStroke(lineWidthF, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND), 1342 AlphaComposite.SrcOver.derive(lineAlphaBLine), 1343 new BasicStroke(lineWidthB, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); 1344 } 1345 1346 // remember draw parameter 1347 heatMapMapViewState = mapViewState; 1348 heatMapCacheLineWith = globalLineWidth; 1349 gpxLayerInvalidated = false; 1350 } 1351 1352 // 4th. Draw data on target layer, map data via color lookup table -------------- 1353 drawHeatMapGrayMap(g, heatMapImgGray, lineWidthB > 2 ? (int) (lineWidthB*1.25f) : 1, lineWidth > 2 ? (lineWidth - 2) : 1); 1354 } 1355 1356 /** 1357 * Draw a dotted heat map 1358 * 1359 * @param gB the common draw object to use 1360 * @param mv the meta data to current displayed area 1361 * @param listSegm segments visible in the current scope of mv 1362 * @param drawSize draw size of draw element 1363 */ 1364 private static void drawHeatGrayDotMap(Graphics2D gB, MapView mv, List<WayPoint> listSegm, int drawSize) { 1365 1366 // typical rendering rate -> use realtime preview instead of accurate display 1367 final double maxSegm = 25_000, nrSegms = listSegm.size(); 1368 1369 // determine random drop rate 1370 final double randomDrop = Math.min(nrSegms > maxSegm ? (nrSegms - maxSegm) / nrSegms : 0, 0.70f); 1371 1372 // http://www.nstb.tc.faa.gov/reports/PAN94_0716.pdf#page=22 1373 // Global Average Position Domain Accuracy, typical -> not worst case ! 1374 // < 4.218 m Vertical 1375 // < 2.168 m Horizontal 1376 final double pixelRmsX = (100 / mv.getDist100Pixel()) * 2.168; 1377 final double pixelRmsY = (100 / mv.getDist100Pixel()) * 4.218; 1378 1379 Point lastPnt = null; 1380 1381 // for all points, draw single lines 1382 for (WayPoint trkPnt : listSegm) { 1383 1384 // get transformed coordinates 1385 final Point paintPnt = mv.getPoint(trkPnt); 1386 1387 // end of line segment or end of list reached 1388 if (trkPnt.drawLine && null != lastPnt) { 1389 drawHeatSurfaceLine(gB, paintPnt, lastPnt, drawSize, pixelRmsX, pixelRmsY, randomDrop); 1390 } 1391 1392 // remember 1393 lastPnt = paintPnt; 1394 } 1395 } 1396 1397 /** 1398 * Draw a dotted surface line 1399 * 1400 * @param g the common draw object to use 1401 * @param fromPnt start point 1402 * @param toPnt end point 1403 * @param drawSize size of draw elements 1404 * @param rmsSizeX RMS size of circle for X (width) 1405 * @param rmsSizeY RMS size of circle for Y (height) 1406 * @param dropRate Pixel render drop rate 1407 */ 1408 private static void drawHeatSurfaceLine(Graphics2D g, 1409 Point fromPnt, Point toPnt, int drawSize, double rmsSizeX, double rmsSizeY, double dropRate) { 1410 1411 // collect frequently used items 1412 final long fromX = (long) fromPnt.getX(); final long deltaX = (long) (toPnt.getX() - fromX); 1413 final long fromY = (long) fromPnt.getY(); final long deltaY = (long) (toPnt.getY() - fromY); 1414 1415 // use same random values for each point 1416 final Random heatMapRandom = new Random(fromX+fromY+deltaX+deltaY); 1417 1418 // cache distance between start and end point 1419 final int dist = (int) Math.abs(fromPnt.distance(toPnt)); 1420 1421 // number of increment ( fill wide distance tracks ) 1422 double scaleStep = Math.max(1.0f / dist, dist > 100 ? 0.10f : 0.20f); 1423 1424 // number of additional random points 1425 int rounds = Math.min(drawSize/2, 1)+1; 1426 1427 // decrease random noise at high drop rate ( more accurate draw of fewer points ) 1428 rmsSizeX *= (1.0d - dropRate); 1429 rmsSizeY *= (1.0d - dropRate); 1430 1431 double scaleVal = 0; 1432 1433 // interpolate line draw ( needs separate point instead of line ) 1434 while (scaleVal < (1.0d-0.0001d)) { 1435 1436 // get position 1437 final double pntX = fromX + scaleVal * deltaX; 1438 final double pntY = fromY + scaleVal * deltaY; 1439 1440 // add random distribution around sampled point 1441 for (int k = 0; k < rounds; k++) { 1442 1443 // add error distribution, first point with less error 1444 int x = (int) (pntX + heatMapRandom.nextGaussian() * (k > 0 ? rmsSizeX : rmsSizeX/4)); 1445 int y = (int) (pntY + heatMapRandom.nextGaussian() * (k > 0 ? rmsSizeY : rmsSizeY/4)); 1446 1447 // draw it, even drop is requested 1448 if (heatMapRandom.nextDouble() >= dropRate) { 1449 g.fillRect(x-drawSize, y-drawSize, drawSize, drawSize); 1450 } 1451 } 1452 scaleVal += scaleStep; 1453 } 1454 } 1455 1456 /** 1457 * Apply default color configuration to way segments 1458 * @param visibleSegments segments visible in the current scope of mv 1459 */ 1460 private void fixColors(List<WayPoint> visibleSegments) { 1461 for (WayPoint trkPnt : visibleSegments) { 1462 if (trkPnt.customColoring == null) { 1463 trkPnt.customColoring = neutralColor; 1464 } 1465 } 1466 } 1467 1468 /** 1469 * Check cache validity set necessary flags 1470 */ 1471 private void checkCache() { 1472 // CHECKSTYLE.OFF: BooleanExpressionComplexity 1473 if ((computeCacheMaxLineLengthUsed != maxLineLength) 1474 || (computeCacheColored != colored) 1475 || (computeCacheVelocityTune != velocityTune) 1476 || (computeCacheColorDynamic != colorModeDynamic) 1477 || (computeCacheHeatMapDrawColorTableIdx != heatMapDrawColorTableIdx) 1478 || !Objects.equals(neutralColor, computeCacheColorUsed) 1479 || (computeCacheHeatMapDrawPointMode != heatMapDrawPointMode) 1480 || (computeCacheHeatMapDrawGain != heatMapDrawGain) 1481 || (computeCacheHeatMapDrawLowerLimit != heatMapDrawLowerLimit) 1482 ) { 1483 // CHECKSTYLE.ON: BooleanExpressionComplexity 1484 computeCacheMaxLineLengthUsed = maxLineLength; 1485 computeCacheInSync = false; 1486 computeCacheColorUsed = neutralColor; 1487 computeCacheColored = colored; 1488 computeCacheVelocityTune = velocityTune; 1489 computeCacheColorDynamic = colorModeDynamic; 1490 computeCacheHeatMapDrawColorTableIdx = heatMapDrawColorTableIdx; 1491 computeCacheHeatMapDrawPointMode = heatMapDrawPointMode; 1492 computeCacheHeatMapDrawGain = heatMapDrawGain; 1493 computeCacheHeatMapDrawLowerLimit = heatMapDrawLowerLimit; 1494 } 1495 } 1496 1497 /** 1498 * callback when data is changed, invalidate cached configuration parameters 1499 */ 1500 @Override 1501 public void gpxDataChanged(GpxDataChangeEvent e) { 1502 computeCacheInSync = false; 1503 } 1504 1505 /** 1506 * Draw all GPX arrays 1507 * @param g the common draw object to use 1508 * @param mv the meta data to current displayed area 1509 */ 1510 public void drawColorBar(Graphics2D g, MapView mv) { 1511 int w = mv.getWidth(); 1512 1513 // set do default 1514 g.setComposite(AlphaComposite.SrcOver.derive(1.00f)); 1515 1516 if (colored == ColorMode.HDOP) { 1517 hdopScale.drawColorBar(g, w-30, 50, 20, 100, 1.0); 1518 } else if (colored == ColorMode.QUALITY) { 1519 qualityScale.drawColorBar(g, w-30, 50, 20, 100, 1.0); 1520 } else if (colored == ColorMode.VELOCITY) { 1521 SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement(); 1522 velocityScale.drawColorBar(g, w-30, 50, 20, 100, som.speedValue); 1523 } else if (colored == ColorMode.DIRECTION) { 1524 directionScale.drawColorBar(g, w-30, 50, 20, 100, 180.0/Math.PI); 1525 } 1526 } 1527 1528 @Override 1529 public void paintableInvalidated(PaintableInvalidationEvent event) { 1530 gpxLayerInvalidated = true; 1531 } 1532 1533 @Override 1534 public void detachFromMapView(MapViewEvent event) { 1535 SystemOfMeasurement.removeSoMChangeListener(this); 1536 layer.removeInvalidationListener(this); 1537 data.removeChangeListener(this); 1538 } 1539}