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