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