source: josm/trunk/src/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelper.java@ 11713

Last change on this file since 11713 was 11713, checked in by Don-vip, 7 years ago

add Ant target to run PMD (only few rules for now), fix violations

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