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