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

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

fix #13124 - Enhanced GPS data rendering: add new heatmap mode (patch by kidelo)

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